From 54bc404aae8c83619e388a4026a4911fdea75cb7 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 14 Jul 2021 15:35:38 -0400 Subject: [PATCH 01/17] feature(core) Initial code to import/export metamodel from cto file Signed-off-by: jeromesimeon --- packages/concerto-cli/index.js | 15 + packages/concerto-cli/lib/commands.js | 26 +- packages/concerto-core/api.txt | 10 + packages/concerto-core/changelog.txt | 3 + packages/concerto-core/index.js | 3 + .../concerto-core/lib/introspect/metamodel.js | 381 ++++++++++++++++++ .../concerto-core/lib/introspect/modelfile.js | 179 ++++---- .../concerto-core/test/data/model/person.cto | 63 +++ .../concerto-core/test/data/model/person.json | 220 ++++++++++ .../test/introspect/metamodel.js | 45 +++ packages/concerto-core/types/index.d.ts | 1 + 11 files changed, 868 insertions(+), 78 deletions(-) create mode 100644 packages/concerto-core/lib/introspect/metamodel.js create mode 100644 packages/concerto-core/test/data/model/person.cto create mode 100644 packages/concerto-core/test/data/model/person.json create mode 100644 packages/concerto-core/test/introspect/metamodel.js diff --git a/packages/concerto-cli/index.js b/packages/concerto-cli/index.js index 8d52973fd7..1e793c9bad 100755 --- a/packages/concerto-cli/index.js +++ b/packages/concerto-cli/index.js @@ -145,6 +145,21 @@ require('yargs') Logger.error(err.message); }); }) + .command('export', 'get the cto string from a metamodel', (yargs) => { + yargs.demandOption(['input'], 'Please provide input metamodel'); + yargs.option('input', { + describe: 'JSON to validate', + type: 'string' + }); + }, (argv) => { + return Commands.export(argv.input) + .then((result) => { + Logger.info(result); + }) + .catch((err) => { + Logger.error(err.message); + }); + }) .option('verbose', { alias: 'v', default: false diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index fc4d952309..a4e05b0b44 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -23,6 +23,7 @@ const ModelLoader = require('@accordproject/concerto-core').ModelLoader; const Factory = require('@accordproject/concerto-core').Factory; const Serializer = require('@accordproject/concerto-core').Serializer; const Concerto = require('@accordproject/concerto-core').Concerto; +const MetaModel = require('@accordproject/concerto-core').MetaModel; const FileWriter = require('@accordproject/concerto-tools').FileWriter; const CodeGen = require('@accordproject/concerto-tools').CodeGen; @@ -134,6 +135,16 @@ class Commands { static async compile(target, ctoFiles, output, options) { const modelManager = await ModelLoader.loadModelManager(ctoFiles, options); + // XXX temporary + if (target === 'metamodel') { + 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); + } + let visitor = null; switch(target) { @@ -165,8 +176,7 @@ class Commands { parameters.fileWriter = new FileWriter(output); modelManager.accept(visitor, parameters); return `Compiled to ${target} in '${output}'.`; - } - else { + } else { return 'Unrecognized target: ' + target; } } @@ -184,6 +194,18 @@ class Commands { modelManager.writeModelsToFileSystem(output); return `Loaded external models in '${output}'.`; } + + /** + * Export meta model to CTO string + * + * @param {string} input the metamodel + * @param {string} the CTO string + */ + static async export(input) { + const json = JSON.parse(fs.readFileSync(input, 'utf8')); + const result = MetaModel.modelFromMetaModel(json); + return result; + } } module.exports = Commands; diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 60eef15841..388e7c59d0 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -109,6 +109,14 @@ class ModelFileDownloader { + Promise downloadExternalDependencies(ModelFile[],Object) + Promise runJob(Object,Object) } + + object fieldToMetaModel() + + object relationshipToMetaModel() + + object enumPropertyToMetaModel() + + object declToMetaModel() + + object modelToMetaModel() + + string fieldFromMetaModel() + + string declFromMetaModel() + + string modelFromMetaModel() class ModelFile { + void constructor(ModelManager,string,string) throws IllegalModelException + Boolean isSystemModelFile() @@ -133,6 +141,8 @@ class ModelFile { + ClassDeclaration[] getAllDeclarations() + 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 e320538e61..f899e516e7 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 1.0.5 {febf7c3ec5ac5ec2f7f9fa88bd823555} 2021-07-13 +- Add support for Concerto metamodel with import/export to CTO + Version 1.0.3 {1fe469fe1a79af5d5a4f5ec7dee6b7d4} 2021-06-25 - Aligns JSDoc and the TypeScript interface diff --git a/packages/concerto-core/index.js b/packages/concerto-core/index.js index ce309c6d0a..11c79045e5 100644 --- a/packages/concerto-core/index.js +++ b/packages/concerto-core/index.js @@ -102,3 +102,6 @@ module.exports.Concerto = require('./lib/concerto'); // Version module.exports.version = require('./package.json'); + +// MetaModel +module.exports.MetaModel = require('./lib/introspect/metamodel'); diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js new file mode 100644 index 0000000000..46cb79f7e9 --- /dev/null +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -0,0 +1,381 @@ +/* + * 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'; + +/** + * Create metamodel for a field + * @param {object} ast - the AST for the field + * @return {object} the metamodel for this field + */ +function fieldToMetaModel(ast) { + const field = {}; + + // Field name + field.name = ast.id.name; + // Is it an array? + if (ast.array) { + field.isArray = true; + } else { + field.isArray = false; + } + // Is it an optional? + if (ast.optional) { + field.isOptional = true; + } else { + field.isOptional = false; + } + // XXX Can it be missing? + const type = ast.propertyType.name; + switch (type) { + case 'Integer': + field.$class = 'concerto.metamodel.IntegerFieldDeclaration'; + break; + case 'Long': + field.$class = 'concerto.metamodel.LongFieldDeclaration'; + break; + case 'Double': + field.$class = 'concerto.metamodel.DoubleFieldDeclaration'; + break; + case 'Boolean': + field.$class = 'concerto.metamodel.BooleanFieldDeclaration'; + break; + case 'DateTime': + field.$class = 'concerto.metamodel.DateTimeFieldDeclaration'; + break; + case 'String': + field.$class = 'concerto.metamodel.StringFieldDeclaration'; + break; + default: + field.$class = 'concerto.metamodel.ObjectFieldDeclaration'; + field.type = { + $class: 'concerto.metamodel.TypeIdentifier', + name: type + }; + break; + } + + return field; +} + +/** + * Create metamodel for a relationship + * @param {object} ast - the AST for the relationtion + * @return {object} the metamodel for this relationship + */ +function relationshipToMetaModel(ast) { + let relationship = { + $class: 'concerto.metamodel.RelationshipDeclaration', + type: { + $class: 'concerto.metamodel.TypeIdentifier', + name: ast.propertyType.name + }, + }; + + // Field name + relationship.name = ast.id.name; + // Is it an array? + if (ast.array) { + relationship.isArray = true; + } else { + relationship.isArray = false; + } + // Is it an optional? + if (ast.optional) { + relationship.isOptional = true; + } else { + relationship.isOptional = false; + } + + return relationship; +} + +/** + * Create metamodel for an enum field + * @param {object} ast - the AST for the enum field + * @return {object} the metamodel for this enum field + */ +function enumPropertyToMetaModel(ast) { + let property = { + $class: 'concerto.metamodel.EnumFieldDeclaration', + }; + + // Field name + property.name = ast.id.name; + // Is it an array? + property.isArray = false; + // Is it an optional? + property.isOptional = false; + + return property; +} + +/** + * Create metamodel for a class declaration + * @param {object} ast - the AST for the declaration + * @return {object} the metamodel for this declaration + */ +function declToMetaModel(ast) { + let decl = {}; + + if(ast.type === 'AssetDeclaration') { + decl.$class = 'concerto.metamodel.AssetDeclaration'; + } else if (ast.type === 'ConceptDeclaration') { + decl.$class = 'concerto.metamodel.ConceptDeclaration'; + } else if (ast.type === 'EventDeclaration') { + decl.$class = 'concerto.metamodel.EventDeclaration'; + } else if (ast.type === 'ParticipantDeclaration') { + decl.$class = 'concerto.metamodel.ParticipantDeclaration'; + } else if (ast.type === 'TransactionDeclaration') { + decl.$class = 'concerto.metamodel.TransactionDeclaration'; + } else if (ast.type === 'EnumDeclaration') { + decl.$class = 'concerto.metamodel.EnumDeclaration'; + } + + // The class name + decl.name = ast.id.name; + + // Is the class abstract? + if (ast.abstract) { + decl.isAbstract = true; + } else { + decl.isAbstract = false; + } + + // Super type + if (ast.classExtension) { + const cname = ast.classExtension.class.name; + if (cname !== 'Asset' && + cname !== 'Concept' && + cname !== 'Event' && + cname !== 'Participant' && + cname !== 'Transaction') { + decl.superType = { + $class: 'concerto.metamodel.TypeIdentifier', + name: ast.classExtension.class.name + }; + } + } + + // Is the class idenfitied by a field + if (ast.idField) { + if (ast.idField.name === '$identifier') { + decl.identified = { + $class: 'concerto.metamodel.Identified' + }; + } else { + decl.identified = { + $class: 'concerto.metamodel.IdentifiedBy', + name: ast.idField.name + }; + } + } + + // Class fields + decl.fields = []; + for (let n = 0; n < ast.body.declarations.length; n++) { + let thing = ast.body.declarations[n]; + + if (thing.type === 'FieldDeclaration') { + decl.fields.push(fieldToMetaModel(thing)); + } else if (thing.type === 'RelationshipDeclaration') { + decl.fields.push(relationshipToMetaModel(thing)); + } else if (thing.type === 'EnumPropertyDeclaration') { + decl.fields.push(enumPropertyToMetaModel(thing)); + } + } + + return decl; +} + +/** + * Export metamodel + * @param {object} ast - the AST for the model + * @return {object} the metamodel for this model + */ +function modelToMetaModel(ast) { + const metamodel = { + $class: 'concerto.metamodel.ModelFile' + }; + metamodel.namespace = ast.namespace; + + if(ast.imports) { + metamodel.imports = []; + ast.imports.forEach((imp) => { + const split = imp.namespace.split('.'); + const name = split.pop(); + const namespace = split.join('.'); + if (namespace === 'concerto') { + return; + } + const ns = { namespace }; + if (name === '*') { + ns.$class = 'concerto.metamodel.ImportAll'; + } else { + ns.$class = 'concerto.metamodel.ImportType'; + ns.identifier = { + $class: 'concerto.metamodel.TypeIdentifier', + name + }; + } + if(imp.uri) { + ns.uri = imp.uri; + } + metamodel.imports.push(ns); + }); + } + + if (ast.body.length > 0) { + metamodel.declarations = []; + } + for(let n=0; n < ast.body.length; n++ ) { + const thing = ast.body[n]; + const decl = declToMetaModel(thing); + metamodel.declarations.push(decl); + } + return metamodel; +} + +/** + * Create a field string from a metamodel + * @param {object} mm - the metamodel + * @return {string} the string for that field + */ +function fieldFromMetaModel(mm) { + let result = ''; + if (mm.$class === 'concerto.metamodel.RelationshipDeclaration') { + result += '-->'; + } else { + result += 'o'; + } + switch (mm.$class) { + case 'concerto.metamodel.EnumFieldDeclaration': + break; + case 'concerto.metamodel.BooleanFieldDeclaration': + result += ' Boolean'; + break; + case 'concerto.metamodel.DateTimeFieldDeclaration': + result += ' DateTime'; + break; + case 'concerto.metamodel.DoubleFieldDeclaration': + result += ' Double'; + break; + case 'concerto.metamodel.IntegerFieldDeclaration': + result += ' Integer'; + break; + case 'concerto.metamodel.LongFieldDeclaration': + result += ' Long'; + break; + case 'concerto.metamodel.StringFieldDeclaration': + result += ' String'; + break; + case 'concerto.metamodel.ObjectFieldDeclaration': + result += ` ${mm.type.name}`; + break; + case 'concerto.metamodel.RelationshipDeclaration': + result += ` ${mm.type.name}`; + break; + } + if (mm.isArray) { + result += '[]'; + } + result += ` ${mm.name}`; + if (mm.isOptional) { + result += ' optional'; + } + return result; +} + +/** + * Create a declaration string from a metamodel + * @param {object} mm - the metamodel + * @return {string} the string for that declaration + */ +function declFromMetaModel(mm) { + let result = ''; + if (mm.isAbstract) { + result += 'abstract '; + } + switch (mm.$class) { + case 'concerto.metamodel.AssetDeclaration': + result += `asset ${mm.name} `; + break; + case 'concerto.metamodel.ConceptDeclaration': + result += `concept ${mm.name} `; + break; + case 'concerto.metamodel.EventDeclaration': + result += `event ${mm.name} `; + break; + case 'concerto.metamodel.ParticipantDeclaration': + result += `participant ${mm.name} `; + break; + case 'concerto.metamodel.TransactionDeclaration': + result += `transaction ${mm.name} `; + break; + case 'concerto.metamodel.EnumDeclaration': + result += `enum ${mm.name} `; + break; + } + if (mm.superType) { + result += `extends ${mm.superType.name} `; + } + // XXX Needs to be fixed to support `identified` + if (mm.identified) { + if (mm.identified.$class === 'concerto.metamodel.IdentifiedBy') { + result += `identified by ${mm.identified.name} `; + } else { + result += 'identified '; + } + } + result += '{'; + mm.fields.forEach((field) => { + result += `\n ${fieldFromMetaModel(field)}`; + }); + result += '\n}'; + return result; +} + +/** + * Create a model string from a metamodel + * @param {object} mm - the metamodel + * @return {string} the string for that model + */ +function modelFromMetaModel(mm) { + let result = ''; + result += `namespace ${mm.namespace}`; + if (mm.imports) { + result += '\n'; + mm.imports.forEach((imp) => { + let name = '*'; + if (imp.$class === 'concerto.metamodel.ImportType') { + name = imp.identifier.name; + } + result += `\nimport ${imp.namespace}.${name}`; + if (imp.uri) { + result += ` from ${imp.uri}`; + } + }); + } + if (mm.declarations) { + mm.declarations.forEach((decl) => { + result += `\n\n${declFromMetaModel(decl)}`; + }); + } + return result; +} + +module.exports = { + modelToMetaModel, + modelFromMetaModel, +}; diff --git a/packages/concerto-core/lib/introspect/modelfile.js b/packages/concerto-core/lib/introspect/modelfile.js index acd5a8d323..0bfb6134c0 100644 --- a/packages/concerto-core/lib/introspect/modelfile.js +++ b/packages/concerto-core/lib/introspect/modelfile.js @@ -17,6 +17,7 @@ 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'); @@ -74,13 +75,6 @@ class ModelFile { try { this.ast = parser.parse(definitions); - if (this.ast.version) { - if (semver.satisfies(packageJson.version, this.ast.version.value)) { - this.concertoVersion = this.ast.version.value; - } else { - throw new Error(`ModelFile expects Concerto version ${this.ast.version.value} but this is ${packageJson.version}`); - } - } } catch(err) { if(err.location && err.location.start) { @@ -91,75 +85,10 @@ class ModelFile { } } - this.namespace = this.ast.namespace; - - if(this.namespace !== 'concerto' && this.ast.imports) { - this.ast.imports.push( { namespace: 'concerto.Concept'} ); - this.ast.imports.push( { namespace: 'concerto.Asset'} ); - this.ast.imports.push( { namespace: 'concerto.Transaction'} ); - this.ast.imports.push( { namespace: 'concerto.Participant'} ); - this.ast.imports.push( { namespace: 'concerto.Event'} ); - } - - if(this.ast.imports) { - this.ast.imports.forEach((imp) => { - this.imports.push(imp.namespace); - this.importShortNames.set(ModelUtil.getShortName(imp.namespace), imp.namespace); - if (ModelUtil.isWildcardName(imp.namespace)) { - const wildcardNamespace = ModelUtil.getNamespace(imp.namespace); - this.importWildcardNamespaces.push(wildcardNamespace); - } - if(imp.uri) { - this.importUriMap[imp.namespace] = imp.uri; - } - }); - } - - for(let n=0; n < this.ast.body.length; n++ ) { - let thing = this.ast.body[n]; - - if(thing.type === 'AssetDeclaration') { - // Default super type for asset - if (!thing.classExtension) { - thing.classExtension = { class: { name: 'Asset' } }; - } - this.declarations.push( new AssetDeclaration(this, thing) ); - } - else if(thing.type === 'TransactionDeclaration') { - // Default super type for transaction - if (!thing.classExtension) { - thing.classExtension = { class: { name: 'Transaction' } }; - } - this.declarations.push( new TransactionDeclaration(this, thing) ); - } - else if(thing.type === 'EventDeclaration') { - // Default super type for event - if (!thing.classExtension) { - thing.classExtension = { class: { name: 'Event' } }; - } - this.declarations.push( new EventDeclaration(this, thing) ); - } - else if(thing.type === 'ParticipantDeclaration') { - // Default super type for participant - if (!thing.classExtension) { - thing.classExtension = { class: { name: 'Participant' } }; - } - this.declarations.push( new ParticipantDeclaration(this, thing) ); - } - else if(thing.type === 'EnumDeclaration') { - this.declarations.push( new EnumDeclaration(this, thing) ); - } - else if(thing.type === 'ConceptDeclaration') { - this.declarations.push( new ConceptDeclaration(this, thing) ); - } - else { - let formatter = Globalize('en').messageFormatter('modelfile-constructor-unrecmodelelem'); - - throw new IllegalModelException(formatter({ - 'type': thing.type, - }),this); - } - } + // Populate from the AST + this.fromAst(this.ast); + // Check version compatibility + this.isCompatibleVersion(); // Now build local types from Declarations for(let index in this.declarations) { @@ -631,6 +560,104 @@ class ModelFile { return this.concertoVersion; } + /** + * Check whether this modelfile is compatible with the concerto version + */ + isCompatibleVersion() { + if (this.ast.version) { + if (semver.satisfies(packageJson.version, this.ast.version.value)) { + this.concertoVersion = this.ast.version.value; + } else { + throw new Error(`ModelFile expects Concerto version ${this.ast.version.value} but this is ${packageJson.version}`); + } + } + } + + /** + * 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 + * @private + */ + fromAst(ast) { + this.namespace = this.ast.namespace; + + if(this.namespace !== 'concerto' && this.ast.imports) { + this.ast.imports.push( { namespace: 'concerto.Concept'} ); + this.ast.imports.push( { namespace: 'concerto.Asset'} ); + this.ast.imports.push( { namespace: 'concerto.Transaction'} ); + this.ast.imports.push( { namespace: 'concerto.Participant'} ); + this.ast.imports.push( { namespace: 'concerto.Event'} ); + } + + if(this.ast.imports) { + this.ast.imports.forEach((imp) => { + this.imports.push(imp.namespace); + this.importShortNames.set(ModelUtil.getShortName(imp.namespace), imp.namespace); + if (ModelUtil.isWildcardName(imp.namespace)) { + const wildcardNamespace = ModelUtil.getNamespace(imp.namespace); + this.importWildcardNamespaces.push(wildcardNamespace); + } + if(imp.uri) { + this.importUriMap[imp.namespace] = imp.uri; + } + }); + } + + for(let n=0; n < this.ast.body.length; n++ ) { + let thing = this.ast.body[n]; + + if(thing.type === 'AssetDeclaration') { + // Default super type for asset + if (!thing.classExtension) { + thing.classExtension = { class: { name: 'Asset' } }; + } + this.declarations.push( new AssetDeclaration(this, thing) ); + } + else if(thing.type === 'TransactionDeclaration') { + // Default super type for transaction + if (!thing.classExtension) { + thing.classExtension = { class: { name: 'Transaction' } }; + } + this.declarations.push( new TransactionDeclaration(this, thing) ); + } + else if(thing.type === 'EventDeclaration') { + // Default super type for event + if (!thing.classExtension) { + thing.classExtension = { class: { name: 'Event' } }; + } + this.declarations.push( new EventDeclaration(this, thing) ); + } + else if(thing.type === 'ParticipantDeclaration') { + // Default super type for participant + if (!thing.classExtension) { + thing.classExtension = { class: { name: 'Participant' } }; + } + this.declarations.push( new ParticipantDeclaration(this, thing) ); + } + else if(thing.type === 'EnumDeclaration') { + this.declarations.push( new EnumDeclaration(this, thing) ); + } + else if(thing.type === 'ConceptDeclaration') { + this.declarations.push( new ConceptDeclaration(this, thing) ); + } + else { + let formatter = Globalize('en').messageFormatter('modelfile-constructor-unrecmodelelem'); + + throw new IllegalModelException(formatter({ + 'type': thing.type, + }),this); + } + } + } + /** * Alternative to instanceof that is reliable across different module instances * @see https://github.com/hyperledger/composer-concerto/issues/47 diff --git a/packages/concerto-core/test/data/model/person.cto b/packages/concerto-core/test/data/model/person.cto new file mode 100644 index 0000000000..44cdc05aa2 --- /dev/null +++ b/packages/concerto-core/test/data/model/person.cto @@ -0,0 +1,63 @@ +/* + * 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. + */ + +// Test CTO for the metamodel support + +namespace test.person + +import org.accordproject.time.* from https://models.accordproject.org/time@0.2.0.cto +import org.accordproject.time.TemporalUnit from https://models.accordproject.org/time@0.2.0.cto + +enum Gender { + o MALE + o FEMALE + o OTHER +} + +abstract participant Individual { +} + +participant Person extends Individual { + o String firstName + o String lastName + o Address address + --> Address address2 + --> Address[] address3 + o Gender gender + o DateTime dob optional +} + +participant Employee extends Person { + o String company + o Boolean onLeave +} + +concept Address identified { + o Integer zip + o Long zip2 + o Double zip3 + o String city + o String country + o String[] street +} + +asset A { +} + +event E { +} + +transaction T identified by id { + o String id +} diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json new file mode 100644 index 0000000000..ff1e7339e9 --- /dev/null +++ b/packages/concerto-core/test/data/model/person.json @@ -0,0 +1,220 @@ +{ + "$class": "concerto.metamodel.ModelFile", + "namespace": "test.person", + "imports": [ + { + "namespace": "org.accordproject.time", + "$class": "concerto.metamodel.ImportAll", + "uri": "https://models.accordproject.org/time@0.2.0.cto" + }, + { + "namespace": "org.accordproject.time", + "$class": "concerto.metamodel.ImportType", + "identifier": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "TemporalUnit" + }, + "uri": "https://models.accordproject.org/time@0.2.0.cto" + } + ], + "declarations": [ + { + "$class": "concerto.metamodel.EnumDeclaration", + "name": "Gender", + "isAbstract": false, + "fields": [ + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "MALE", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "FEMALE", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "OTHER", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Individual", + "isAbstract": true, + "fields": [] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Person", + "isAbstract": false, + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Individual" + }, + "fields": [ + { + "name": "firstName", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "name": "lastName", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "name": "address", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + } + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + }, + "name": "address2", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + }, + "name": "address3", + "isArray": true, + "isOptional": false + }, + { + "name": "gender", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Gender" + } + }, + { + "name": "dob", + "isArray": false, + "isOptional": true, + "$class": "concerto.metamodel.DateTimeFieldDeclaration" + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Employee", + "isAbstract": false, + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Person" + }, + "fields": [ + { + "name": "company", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "name": "onLeave", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.BooleanFieldDeclaration" + } + ] + }, + { + "$class": "concerto.metamodel.ConceptDeclaration", + "name": "Address", + "isAbstract": false, + "identified": { + "$class": "concerto.metamodel.Identified" + }, + "fields": [ + { + "name": "zip", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.IntegerFieldDeclaration" + }, + { + "name": "zip2", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.LongFieldDeclaration" + }, + { + "name": "zip3", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.DoubleFieldDeclaration" + }, + { + "name": "city", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "name": "country", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "name": "street", + "isArray": true, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "name": "A", + "isAbstract": false, + "fields": [] + }, + { + "$class": "concerto.metamodel.EventDeclaration", + "name": "E", + "isAbstract": false, + "fields": [] + }, + { + "$class": "concerto.metamodel.TransactionDeclaration", + "name": "T", + "isAbstract": false, + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "id" + }, + "fields": [ + { + "name": "id", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + } + ] + } + ] +} diff --git a/packages/concerto-core/test/introspect/metamodel.js b/packages/concerto-core/test/introspect/metamodel.js new file mode 100644 index 0000000000..a54c63ccb8 --- /dev/null +++ b/packages/concerto-core/test/introspect/metamodel.js @@ -0,0 +1,45 @@ +/* + * 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 ModelManager = require('../../lib/modelmanager'); +const ModelFile = require('../../lib/introspect/modelfile'); +const MetaModel = require('../../lib/introspect/metamodel'); +const fs = require('fs'); +const path = require('path'); + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-things')); + +describe('MetaModel', () => { + const personModel = fs.readFileSync(path.resolve(__dirname, '../data/model/person.cto'), 'utf8'); + const personMetaModel = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/model/person.json'), 'utf8')); + + describe('#toMetaModel', () => { + + it('should convert CTO file to its metamodel', () => { + const modelManager1 = new ModelManager(); + const mf1 = new ModelFile(modelManager1, personModel); + const mm1 = mf1.toMetaModel(); + mm1.should.deep.equal(personMetaModel); + const model2 = MetaModel.modelFromMetaModel(mm1); + const modelManager2 = new ModelManager(); + const mf2 = new ModelFile(modelManager2, model2); + const mm2 = mf2.toMetaModel(); + mm2.should.deep.equal(personMetaModel); + }); + }); +}); diff --git a/packages/concerto-core/types/index.d.ts b/packages/concerto-core/types/index.d.ts index c40a5aa476..62879d6879 100644 --- a/packages/concerto-core/types/index.d.ts +++ b/packages/concerto-core/types/index.d.ts @@ -247,6 +247,7 @@ declare module '@accordproject/concerto-core' { // ModelFile export class ModelFile { constructor(modelManager: ModelManager, definitions: string, fileName?: string); + private fromAst(ast: any): void; isSystemModelFile(): boolean; isExternal(): boolean; private getImportURI(namespace: string): string | null; From 318b73433c3b03e38cff4039c94ad5bfc0d0125a Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Thu, 15 Jul 2021 12:24:00 -0400 Subject: [PATCH 02/17] fix(cli) new metamodel transform command for clarity Signed-off-by: jeromesimeon --- packages/concerto-cli/index.js | 8 +- packages/concerto-cli/lib/commands.js | 35 ++++---- packages/concerto-cli/test/cli.js | 15 ++++ .../concerto-cli/test/models/contract.json | 85 +++++++++++++++++++ .../concerto-cli/test/models/contract2.cto | 18 ++++ .../concerto-core/lib/introspect/metamodel.js | 4 +- 6 files changed, 142 insertions(+), 23 deletions(-) create mode 100644 packages/concerto-cli/test/models/contract.json create mode 100644 packages/concerto-cli/test/models/contract2.cto diff --git a/packages/concerto-cli/index.js b/packages/concerto-cli/index.js index 1e793c9bad..12106afe2f 100755 --- a/packages/concerto-cli/index.js +++ b/packages/concerto-cli/index.js @@ -145,14 +145,14 @@ require('yargs') Logger.error(err.message); }); }) - .command('export', 'get the cto string from a metamodel', (yargs) => { - yargs.demandOption(['input'], 'Please provide input metamodel'); + .command('transform', 'get the cto string from a metamodel', (yargs) => { + yargs.demandOption(['input'], 'Please provide input (meta)model'); yargs.option('input', { - describe: 'JSON to validate', + describe: '(meta)model file', type: 'string' }); }, (argv) => { - return Commands.export(argv.input) + return Commands.transform(argv.input) .then((result) => { Logger.info(result); }) diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index a4e05b0b44..40d6a6407f 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -135,16 +135,6 @@ class Commands { static async compile(target, ctoFiles, output, options) { const modelManager = await ModelLoader.loadModelManager(ctoFiles, options); - // XXX temporary - if (target === 'metamodel') { - 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); - } - let visitor = null; switch(target) { @@ -196,15 +186,26 @@ class Commands { } /** - * Export meta model to CTO string + * Convert between CTO string and metamodel * - * @param {string} input the metamodel - * @param {string} the CTO string + * @param {string} input (meta)model + * @param {string} transformed (meta)model */ - static async export(input) { - const json = JSON.parse(fs.readFileSync(input, 'utf8')); - const result = MetaModel.modelFromMetaModel(json); - return result; + 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); + } else { + const inputString = fs.readFileSync(input, 'utf8'); + const json = JSON.parse(inputString); + const result = MetaModel.modelFromMetaModel(json); + return result; + } } } diff --git a/packages/concerto-cli/test/cli.js b/packages/concerto-cli/test/cli.js index e508b2e875..ad97029b36 100644 --- a/packages/concerto-cli/test/cli.js +++ b/packages/concerto-cli/test/cli.js @@ -202,4 +202,19 @@ describe('cicero-cli', () => { dir.cleanup(); }); }); + + describe('#metamodel', async () => { + it('should transform cto to metamodel', async () => { + const expected = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'models/contract.json'))); + const result = JSON.parse(await Commands.transform(path.resolve(__dirname, 'models/contract.cto'))); + result.should.deep.equal(expected); + }); + + it('should transform a metamodel to cto', async () => { + const expected = fs.readFileSync(path.resolve(__dirname, 'models/contract2.cto'), 'utf-8'); + const metamodel = path.resolve(__dirname, 'models/contract.json'); + const result = await Commands.transform(metamodel); + result.should.equal(expected); + }); + }); }); \ No newline at end of file diff --git a/packages/concerto-cli/test/models/contract.json b/packages/concerto-cli/test/models/contract.json new file mode 100644 index 0000000000..ba618f738f --- /dev/null +++ b/packages/concerto-cli/test/models/contract.json @@ -0,0 +1,85 @@ +{ + "$class": "concerto.metamodel.ModelFile", + "namespace": "org.accordproject.cicero.contract", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel.AssetDeclaration", + "name": "AccordContractState", + "isAbstract": false, + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "stateId" + }, + "fields": [ + { + "name": "stateId", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "AccordParty", + "isAbstract": false, + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "partyId" + }, + "fields": [ + { + "name": "partyId", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "name": "AccordContract", + "isAbstract": true, + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "contractId" + }, + "fields": [ + { + "name": "contractId", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "AccordParty" + }, + "name": "parties", + "isArray": true, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "name": "AccordClause", + "isAbstract": true, + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "clauseId" + }, + "fields": [ + { + "name": "clauseId", + "isArray": false, + "isOptional": false, + "$class": "concerto.metamodel.StringFieldDeclaration" + } + ] + } + ] +} diff --git a/packages/concerto-cli/test/models/contract2.cto b/packages/concerto-cli/test/models/contract2.cto new file mode 100644 index 0000000000..fe42230ec3 --- /dev/null +++ b/packages/concerto-cli/test/models/contract2.cto @@ -0,0 +1,18 @@ +namespace org.accordproject.cicero.contract + +asset AccordContractState identified by stateId { + o String stateId +} + +participant AccordParty identified by partyId { + o String partyId +} + +abstract asset AccordContract identified by contractId { + o String contractId + --> AccordParty[] parties optional +} + +abstract asset AccordClause identified by clauseId { + o String clauseId +} \ No newline at end of file diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index 46cb79f7e9..7beda56732 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -354,7 +354,7 @@ function declFromMetaModel(mm) { function modelFromMetaModel(mm) { let result = ''; result += `namespace ${mm.namespace}`; - if (mm.imports) { + if (mm.imports && mm.imports.length > 0) { result += '\n'; mm.imports.forEach((imp) => { let name = '*'; @@ -367,7 +367,7 @@ function modelFromMetaModel(mm) { } }); } - if (mm.declarations) { + if (mm.declarations && mm.declarations.length > 0) { mm.declarations.forEach((decl) => { result += `\n\n${declFromMetaModel(decl)}`; }); From e844034eaa82bee62e82b36244809faf98c2e11e Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 19 Jul 2021 08:48:10 -0400 Subject: [PATCH 03/17] 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); }); }); From a13a723af758281b9ce9998a4331164ae44a0dc8 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 19 Jul 2021 09:19:03 -0400 Subject: [PATCH 04/17] feature(metamodel) Option to turn validation off (for the editor) Signed-off-by: jeromesimeon --- .../concerto-core/lib/introspect/metamodel.js | 18 +++++++++++------- .../concerto-core/test/introspect/metamodel.js | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index b75c7d8d78..bbd36744f9 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -442,9 +442,10 @@ function declToMetaModel(ast) { /** * Export metamodel from an AST * @param {object} ast - the AST for the model + * @param {boolean} [validate] - whether to perform validation * @return {object} the metamodel for this model */ -function modelToMetaModel(ast) { +function modelToMetaModel(ast, validate = true) { const metamodel = { $class: 'concerto.metamodel.ModelFile' }; @@ -486,7 +487,7 @@ function modelToMetaModel(ast) { } // Last, validate the JSON metaModel - const mm = validateMetaModel(metamodel); + const mm = validate ? validateMetaModel(metamodel) : metamodel; return mm; } @@ -494,10 +495,11 @@ function modelToMetaModel(ast) { /** * Export metamodel from a model file * @param {object} modelFile - the AST for the model + * @param {boolean} [validate] - whether to perform validation * @return {object} the metamodel for this model */ -function modelFileToMetaModel(modelFile) { - return modelToMetaModel(modelFile.ast); +function modelFileToMetaModel(modelFile, validate) { + return modelToMetaModel(modelFile.ast, validate); } /** @@ -602,11 +604,12 @@ function declFromMetaModel(mm) { /** * Create a model string from a metamodel * @param {object} metaModel - the metamodel + * @param {boolean} [validate] - whether to perform validation * @return {string} the string for that model */ -function ctoFromMetaModel(metaModel) { +function ctoFromMetaModel(metaModel, validate = true) { // First, validate the JSON metaModel - const mm = validateMetaModel(metaModel); + const mm = validate ? validateMetaModel(metaModel) : metaModel; let result = ''; result += `namespace ${mm.namespace}`; @@ -634,9 +637,10 @@ function ctoFromMetaModel(metaModel) { /** * Export metamodel from a model string * @param {object} model - the string for the model + * @param {boolean} [validate] - whether to perform validation * @return {object} the metamodel for this model */ -function ctoToMetaModel(model) { +function ctoToMetaModel(model, validate) { const ast = parser.parse(model); return modelToMetaModel(ast); } diff --git a/packages/concerto-core/test/introspect/metamodel.js b/packages/concerto-core/test/introspect/metamodel.js index 0912de44a2..484938860b 100644 --- a/packages/concerto-core/test/introspect/metamodel.js +++ b/packages/concerto-core/test/introspect/metamodel.js @@ -30,11 +30,28 @@ describe('MetaModel', () => { describe('#toMetaModel', () => { it('should convert a CTO model to its metamodel', () => { + const mm1 = MetaModel.ctoToMetaModel(personModel, false); + mm1.should.deep.equal(personMetaModel); + }); + + it('should convert and validate a CTO model to its metamodel', () => { const mm1 = MetaModel.ctoToMetaModel(personModel); mm1.should.deep.equal(personMetaModel); }); it('should convert a ModelFile to its metamodel', () => { + const modelManager1 = new ModelManager(); + const mf1 = new ModelFile(modelManager1, personModel); + const mm1 = MetaModel.modelFileToMetaModel(mf1, false); + mm1.should.deep.equal(personMetaModel); + const model2 = MetaModel.ctoFromMetaModel(mm1, false); + const modelManager2 = new ModelManager(); + const mf2 = new ModelFile(modelManager2, model2); + const mm2 = MetaModel.modelFileToMetaModel(mf2, false); + mm2.should.deep.equal(personMetaModel); + }); + + it('should convert and validate a ModelFile to its metamodel', () => { const modelManager1 = new ModelManager(); const mf1 = new ModelFile(modelManager1, personModel); const mm1 = MetaModel.modelFileToMetaModel(mf1); From 8ba82af385ba6a64040f7db868d35ff75b3fa302 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 19 Jul 2021 11:48:39 -0400 Subject: [PATCH 05/17] feature(metamodel) Add support for default values Signed-off-by: jeromesimeon --- .../concerto-core/lib/introspect/metamodel.js | 121 ++++++++------ .../concerto-core/test/data/model/person.cto | 24 ++- .../concerto-core/test/data/model/person.json | 151 +++++++++++++----- 3 files changed, 202 insertions(+), 94 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index bbd36744f9..73dc0d0eb0 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -107,26 +107,6 @@ 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 @@ -141,19 +121,24 @@ abstract concept FieldDeclaration { o Decorator[] decorators optional } -concept ObjectFieldDeclaration extends FieldDeclaration { - @FormEditor("hide", true) - o StringDefault defaultValue optional +concept EnumFieldDeclaration extends FieldDeclaration { +} + +concept RelationshipDeclaration extends FieldDeclaration { @FormEditor("title", "Type Name", "selectOptions", "types") o TypeIdentifier type } -concept EnumFieldDeclaration extends FieldDeclaration { +concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o String defaultValue optional + @FormEditor("title", "Type Name", "selectOptions", "types") + o TypeIdentifier type } concept BooleanFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) - o BooleanDefault defaultValue optional + o Boolean defaultValue optional } concept DateTimeFieldDeclaration extends FieldDeclaration { @@ -161,7 +146,7 @@ concept DateTimeFieldDeclaration extends FieldDeclaration { concept StringFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) - o StringDefault defaultValue optional + o String defaultValue optional @FormEditor("hide", true) o StringRegexValidator validator optional } @@ -170,43 +155,38 @@ concept StringRegexValidator { o String regex } +concept DoubleFieldDeclaration extends FieldDeclaration { + o Double defaultValue optional + o DoubleDomainValidator validator optional +} + 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 + o Integer defaultValue optional @FormEditor("hide", true) o IntegerDomainValidator validator optional } +concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional +} + concept LongFieldDeclaration extends FieldDeclaration { @FormEditor("hide", true) - o LongDefault defaultValue optional + o Long defaultValue optional @FormEditor("hide", true) o LongDomainValidator validator optional } -concept RelationshipDeclaration extends FieldDeclaration { - @FormEditor("title", "Type Name", "selectOptions", "types") - o TypeIdentifier type +concept LongDomainValidator { + o Long lower optional + o Long upper optional } abstract concept Import { @@ -260,6 +240,7 @@ function validateMetaModel(input) { * @return {object} the metamodel for this field */ function fieldToMetaModel(ast) { + // console.log(`FIELD ${JSON.stringify(ast)}`); const field = {}; // Field name @@ -281,24 +262,46 @@ function fieldToMetaModel(ast) { switch (type) { case 'Integer': field.$class = 'concerto.metamodel.IntegerFieldDeclaration'; + if (ast.default) { + field.defaultValue = parseInt(ast.default); + } break; case 'Long': field.$class = 'concerto.metamodel.LongFieldDeclaration'; + if (ast.default) { + field.defaultValue = parseInt(ast.default); + } break; case 'Double': field.$class = 'concerto.metamodel.DoubleFieldDeclaration'; + if (ast.default) { + field.defaultValue = parseFloat(ast.default); + } break; case 'Boolean': field.$class = 'concerto.metamodel.BooleanFieldDeclaration'; + if (ast.default) { + if (ast.default === 'true') { + field.defaultValue = true; + } else { + field.defaultValue = false; + } + } break; case 'DateTime': field.$class = 'concerto.metamodel.DateTimeFieldDeclaration'; break; case 'String': field.$class = 'concerto.metamodel.StringFieldDeclaration'; + if (ast.default) { + field.defaultValue = ast.default; + } break; default: field.$class = 'concerto.metamodel.ObjectFieldDeclaration'; + if (ast.default) { + field.defaultValue = ast.default; + } field.type = { $class: 'concerto.metamodel.TypeIdentifier', name: type @@ -509,6 +512,7 @@ function modelFileToMetaModel(modelFile, validate) { */ function fieldFromMetaModel(mm) { let result = ''; + let defaultString = ''; if (mm.$class === 'concerto.metamodel.RelationshipDeclaration') { result += '-->'; } else { @@ -519,24 +523,48 @@ function fieldFromMetaModel(mm) { break; case 'concerto.metamodel.BooleanFieldDeclaration': result += ' Boolean'; + if (mm.defaultValue === true || mm.defaultValue === false) { + if (mm.defaultValue) { + defaultString += ' default=true'; + } else { + defaultString += ' default=false'; + } + } break; case 'concerto.metamodel.DateTimeFieldDeclaration': result += ' DateTime'; break; case 'concerto.metamodel.DoubleFieldDeclaration': result += ' Double'; + if (mm.defaultValue) { + const doubleString = mm.defaultValue.toFixed(Math.max(1, (mm.defaultValue.toString().split('.')[1] || []).length)); + + defaultString += ` default=${doubleString}`; + } break; case 'concerto.metamodel.IntegerFieldDeclaration': result += ' Integer'; + if (mm.defaultValue) { + defaultString += ` default=${mm.defaultValue.toString()}`; + } break; case 'concerto.metamodel.LongFieldDeclaration': result += ' Long'; + if (mm.defaultValue) { + defaultString += ` default=${mm.defaultValue.toString()}`; + } break; case 'concerto.metamodel.StringFieldDeclaration': result += ' String'; + if (mm.defaultValue) { + defaultString += ` default="${mm.defaultValue}"`; + } break; case 'concerto.metamodel.ObjectFieldDeclaration': result += ` ${mm.type.name}`; + if (mm.defaultValue) { + defaultString += ` default="${mm.defaultValue}"`; + } break; case 'concerto.metamodel.RelationshipDeclaration': result += ` ${mm.type.name}`; @@ -549,6 +577,7 @@ function fieldFromMetaModel(mm) { if (mm.isOptional) { result += ' optional'; } + result += defaultString; return result; } diff --git a/packages/concerto-core/test/data/model/person.cto b/packages/concerto-core/test/data/model/person.cto index 44cdc05aa2..a48c6b1deb 100644 --- a/packages/concerto-core/test/data/model/person.cto +++ b/packages/concerto-core/test/data/model/person.cto @@ -32,8 +32,10 @@ participant Person extends Individual { o String firstName o String lastName o Address address - --> Address address2 - --> Address[] address3 + o Address address2 default="USAddress" + --> Address address3 + --> Address[] address4 + --> Address address5 optional o Gender gender o DateTime dob optional } @@ -44,12 +46,20 @@ participant Employee extends Person { } concept Address identified { - o Integer zip - o Long zip2 - o Double zip3 - o String city - o String country + o Integer zip default=10001 + o Long zip2 default=10001 + o Double zip3 default=10001.0 + o String city default="NYC" + o String country default="USA" o String[] street + o Boolean isResidential default=true + o Boolean isPrivate default=false +} + +concept USAddress extends Address { + o Integer zip4 + o Long zip5 + o Double zip6 } asset A { diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index ff1e7339e9..ea80a8fd8d 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -3,25 +3,25 @@ "namespace": "test.person", "imports": [ { - "namespace": "org.accordproject.time", "$class": "concerto.metamodel.ImportAll", + "namespace": "org.accordproject.time", "uri": "https://models.accordproject.org/time@0.2.0.cto" }, { - "namespace": "org.accordproject.time", "$class": "concerto.metamodel.ImportType", "identifier": { "$class": "concerto.metamodel.TypeIdentifier", "name": "TemporalUnit" }, + "namespace": "org.accordproject.time", "uri": "https://models.accordproject.org/time@0.2.0.cto" } ], "declarations": [ { "$class": "concerto.metamodel.EnumDeclaration", - "name": "Gender", "isAbstract": false, + "name": "Gender", "fields": [ { "$class": "concerto.metamodel.EnumFieldDeclaration", @@ -45,40 +45,51 @@ }, { "$class": "concerto.metamodel.ParticipantDeclaration", - "name": "Individual", "isAbstract": true, + "name": "Individual", "fields": [] }, { "$class": "concerto.metamodel.ParticipantDeclaration", - "name": "Person", "isAbstract": false, + "name": "Person", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Individual" }, "fields": [ { + "$class": "concerto.metamodel.StringFieldDeclaration", "name": "firstName", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.StringFieldDeclaration", "name": "lastName", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + }, "name": "address", "isArray": false, - "isOptional": false, + "isOptional": false + }, + { "$class": "concerto.metamodel.ObjectFieldDeclaration", + "defaultValue": "USAddress", "type": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Address" - } + }, + "name": "address2", + "isArray": false, + "isOptional": false }, { "$class": "concerto.metamodel.RelationshipDeclaration", @@ -86,7 +97,7 @@ "$class": "concerto.metamodel.TypeIdentifier", "name": "Address" }, - "name": "address2", + "name": "address3", "isArray": false, "isOptional": false }, @@ -96,123 +107,181 @@ "$class": "concerto.metamodel.TypeIdentifier", "name": "Address" }, - "name": "address3", + "name": "address4", "isArray": true, "isOptional": false }, { - "name": "gender", + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + }, + "name": "address5", "isArray": false, - "isOptional": false, + "isOptional": true + }, + { "$class": "concerto.metamodel.ObjectFieldDeclaration", "type": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Gender" - } + }, + "name": "gender", + "isArray": false, + "isOptional": false }, { + "$class": "concerto.metamodel.DateTimeFieldDeclaration", "name": "dob", "isArray": false, - "isOptional": true, - "$class": "concerto.metamodel.DateTimeFieldDeclaration" + "isOptional": true } ] }, { "$class": "concerto.metamodel.ParticipantDeclaration", - "name": "Employee", "isAbstract": false, + "name": "Employee", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Person" }, "fields": [ { + "$class": "concerto.metamodel.StringFieldDeclaration", "name": "company", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.BooleanFieldDeclaration", "name": "onLeave", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.BooleanFieldDeclaration" + "isOptional": false } ] }, { "$class": "concerto.metamodel.ConceptDeclaration", - "name": "Address", "isAbstract": false, + "name": "Address", "identified": { "$class": "concerto.metamodel.Identified" }, "fields": [ { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "defaultValue": 10001, "name": "zip", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.IntegerFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.LongFieldDeclaration", + "defaultValue": 10001, "name": "zip2", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.LongFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "defaultValue": 10001, "name": "zip3", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.DoubleFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.StringFieldDeclaration", + "defaultValue": "NYC", "name": "city", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.StringFieldDeclaration", + "defaultValue": "USA", "name": "country", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false }, { + "$class": "concerto.metamodel.StringFieldDeclaration", "name": "street", "isArray": true, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false + }, + { + "$class": "concerto.metamodel.BooleanFieldDeclaration", + "defaultValue": true, + "name": "isResidential", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.BooleanFieldDeclaration", + "defaultValue": false, + "name": "isPrivate", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ConceptDeclaration", + "isAbstract": false, + "name": "USAddress", + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address" + }, + "fields": [ + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "name": "zip4", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "name": "zip5", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "name": "zip6", + "isArray": false, + "isOptional": false } ] }, { "$class": "concerto.metamodel.AssetDeclaration", - "name": "A", "isAbstract": false, + "name": "A", "fields": [] }, { "$class": "concerto.metamodel.EventDeclaration", - "name": "E", "isAbstract": false, + "name": "E", "fields": [] }, { "$class": "concerto.metamodel.TransactionDeclaration", - "name": "T", "isAbstract": false, + "name": "T", "identified": { "$class": "concerto.metamodel.IdentifiedBy", "name": "id" }, "fields": [ { + "$class": "concerto.metamodel.StringFieldDeclaration", "name": "id", "isArray": false, - "isOptional": false, - "$class": "concerto.metamodel.StringFieldDeclaration" + "isOptional": false } ] } From c1331db2c29222c7de38c62efde5d0286436c058 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 21 Jul 2021 12:49:57 -0400 Subject: [PATCH 06/17] fix(core) Update TypeScript interface for MetaModel Signed-off-by: jeromesimeon --- packages/concerto-core/index.js | 6 +++--- packages/concerto-core/types/index.d.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/concerto-core/index.js b/packages/concerto-core/index.js index 11c79045e5..3d0f32b9f6 100644 --- a/packages/concerto-core/index.js +++ b/packages/concerto-core/index.js @@ -100,8 +100,8 @@ module.exports.TypedStack = require('./lib/serializer/typedstack'); // Concerto module.exports.Concerto = require('./lib/concerto'); -// Version -module.exports.version = require('./package.json'); - // MetaModel module.exports.MetaModel = require('./lib/introspect/metamodel'); + +// Version +module.exports.version = require('./package.json'); diff --git a/packages/concerto-core/types/index.d.ts b/packages/concerto-core/types/index.d.ts index 62879d6879..c95f0df3c4 100644 --- a/packages/concerto-core/types/index.d.ts +++ b/packages/concerto-core/types/index.d.ts @@ -448,6 +448,14 @@ declare module '@accordproject/concerto-core' { getNamespace(obj: any): string; } + // MetaModel + export namespace MetaModel { + const metaModelCto: string; + function modelFileToMetaModel(modelFile: ModelFile, validate: boolean): any; + function ctoToMetaModel(model: string, validate: boolean): any; + function ctoFromMetaModel(metaModel: any, validate: boolean): string; + } + // version export const version: any; } From 9d88ab9f65c2dd898f7ff060fc0c44a241355ad2 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Fri, 23 Jul 2021 11:04:40 -0400 Subject: [PATCH 07/17] feature(metamodel) Add ability to resolve type names + better CLI Signed-off-by: jeromesimeon --- packages/concerto-cli/index.js | 38 ++- packages/concerto-cli/lib/commands.js | 39 ++- packages/concerto-cli/test/cli.js | 15 +- .../test/models/contractResolved.json | 86 +++++ packages/concerto-core/api.txt | 4 + packages/concerto-core/changelog.txt | 2 +- .../concerto-core/lib/introspect/metamodel.js | 116 ++++++- .../concerto-core/test/data/model/person.cto | 2 + .../concerto-core/test/data/model/person.json | 25 +- .../test/data/model/personResolved.json | 317 ++++++++++++++++++ .../test/introspect/metamodel.js | 11 +- packages/concerto-core/test/modelloader.js | 4 +- 12 files changed, 625 insertions(+), 34 deletions(-) create mode 100644 packages/concerto-cli/test/models/contractResolved.json create mode 100644 packages/concerto-core/test/data/model/personResolved.json diff --git a/packages/concerto-cli/index.js b/packages/concerto-cli/index.js index 12106afe2f..511a09fa39 100755 --- a/packages/concerto-cli/index.js +++ b/packages/concerto-cli/index.js @@ -145,14 +145,44 @@ require('yargs') Logger.error(err.message); }); }) - .command('transform', 'get the cto string from a metamodel', (yargs) => { - yargs.demandOption(['input'], 'Please provide input (meta)model'); + .command('import', 'import a cto string into its metamodel', (yargs) => { + yargs.demandOption(['input'], 'Please provide an input cto'); + yargs.option('model', { + describe: 'array of concerto (cto) model files', + type: 'string', + array: true + }); + yargs.option('input', { + describe: 'the cto model to import', + type: 'string' + }); + yargs.option('resolve', { + describe: 'resolve names to fully qualified names', + type: 'boolean', + default: false + }); + }, (argv) => { + return Commands.import(argv.input, argv.model, argv.resolve) + .then((result) => { + Logger.info(result); + }) + .catch((err) => { + Logger.error(err.message); + }); + }) + .command('export', 'export a metamodel to cto syntax', (yargs) => { + yargs.demandOption(['input'], 'Please provide an input metamodel'); + yargs.option('model', { + describe: 'array of concerto (cto) model files', + type: 'string', + array: true + }); yargs.option('input', { - describe: '(meta)model file', + describe: 'the metamodel to export', type: 'string' }); }, (argv) => { - return Commands.transform(argv.input) + return Commands.export(argv.input, argv.model) .then((result) => { Logger.info(result); }) diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index 6dc412f68b..691bfd4f8d 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -186,22 +186,37 @@ class Commands { } /** - * Convert between CTO string and metamodel + * Import a CTO string to its metamodel * - * @param {string} input (meta)model - * @param {string} transformed (meta)model + * @param {string} input - CTO + * @param {string[]} ctoFiles - the CTO files used for import resolution + * @param {boolean} resolve - whether to resolve the names + * @param {string} the metamodel */ - static async transform(input) { - if (path.extname(input) === '.cto') { - const inputString = fs.readFileSync(input, 'utf8'); - const result = MetaModel.ctoToMetaModel(inputString); - return JSON.stringify(result); + static async import(input, ctoFiles, resolve) { + const inputString = fs.readFileSync(input, 'utf8'); + let result; + if (resolve) { + const modelManager = await ModelLoader.loadModelManager(ctoFiles); + result = MetaModel.ctoToMetaModelAndResolve(modelManager, inputString); } else { - const inputString = fs.readFileSync(input, 'utf8'); - const json = JSON.parse(inputString); - const result = MetaModel.ctoFromMetaModel(json); - return result; + result = MetaModel.ctoToMetaModel(inputString); } + return JSON.stringify(result); + } + + /** + * Export a metamodel to a CTO string + * + * @param {string} input metamodel + * @param {string[]} ctoFiles - the CTO files used for import resolution + * @param {string} transformed (meta)model + */ + static async export(input, ctoFiles) { + const inputString = fs.readFileSync(input, 'utf8'); + const json = JSON.parse(inputString); + const result = MetaModel.ctoFromMetaModel(json); + return result; } } diff --git a/packages/concerto-cli/test/cli.js b/packages/concerto-cli/test/cli.js index ad97029b36..2fdf2efe87 100644 --- a/packages/concerto-cli/test/cli.js +++ b/packages/concerto-cli/test/cli.js @@ -203,17 +203,26 @@ describe('cicero-cli', () => { }); }); - describe('#metamodel', async () => { + describe('#import', async () => { it('should transform cto to metamodel', async () => { const expected = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'models/contract.json'))); - const result = JSON.parse(await Commands.transform(path.resolve(__dirname, 'models/contract.cto'))); + const result = JSON.parse(await Commands.import(path.resolve(__dirname, 'models/contract.cto'))); result.should.deep.equal(expected); }); + it('should transform cto to metamodel and resolve names', async () => { + const expected = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'models/contractResolved.json'))); + const contractFile = path.resolve(__dirname, 'models/contract.cto'); + const result = JSON.parse(await Commands.import(contractFile, [contractFile], true)); + result.should.deep.equal(expected); + }); + }); + + describe('#export', async () => { it('should transform a metamodel to cto', async () => { const expected = fs.readFileSync(path.resolve(__dirname, 'models/contract2.cto'), 'utf-8'); const metamodel = path.resolve(__dirname, 'models/contract.json'); - const result = await Commands.transform(metamodel); + const result = await Commands.export(metamodel); result.should.equal(expected); }); }); diff --git a/packages/concerto-cli/test/models/contractResolved.json b/packages/concerto-cli/test/models/contractResolved.json new file mode 100644 index 0000000000..069e40bc52 --- /dev/null +++ b/packages/concerto-cli/test/models/contractResolved.json @@ -0,0 +1,86 @@ +{ + "$class": "concerto.metamodel.ModelFile", + "namespace": "org.accordproject.cicero.contract", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel.AssetDeclaration", + "isAbstract": false, + "name": "AccordContractState", + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "stateId" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "stateId", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "isAbstract": false, + "name": "AccordParty", + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "partyId" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "partyId", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "isAbstract": true, + "name": "AccordContract", + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "contractId" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "contractId", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "AccordParty", + "fullyQualifiedName": "org.accordproject.cicero.contract.AccordParty" + }, + "name": "parties", + "isArray": true, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "isAbstract": true, + "name": "AccordClause", + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "clauseId" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "clauseId", + "isArray": false, + "isOptional": false + } + ] + } + ] +} diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 782106e24f..aad6734e46 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -111,6 +111,9 @@ class ModelFileDownloader { } + void createMetaModelManager() + object validateMetaModel() + + object createNameTable() + + string resolveName() + + object resolveTypeNames() + object fieldToMetaModel() + object relationshipToMetaModel() + object enumPropertyToMetaModel() @@ -121,6 +124,7 @@ class ModelFileDownloader { + string declFromMetaModel() + string ctoFromMetaModel() + object ctoToMetaModel() + + object ctoToMetaModelAndResolve() class ModelFile { + void constructor(ModelManager,string,string) throws IllegalModelException + Boolean isSystemModelFile() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 04ef820c8a..bc9c8248ae 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 {0717bf67bc6e667299832b5beb40fd90} 2021-07-13 +Version 1.0.5 {e441f02829c80dd1b4409c71c8ddd28f} 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 73dc0d0eb0..3208493e1b 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -56,6 +56,7 @@ concept DecoratorBoolean extends DecoratorLiteral { concept TypeIdentifier { @FormEditor("selectOptions", "types") o String name default="Concept" + o String fullyQualifiedName optional } concept DecoratorIdentifier extends DecoratorLiteral { @@ -198,7 +199,7 @@ concept ImportAll extends Import { } concept ImportType extends Import { - o TypeIdentifier identifier + o String name } concept ModelFile { @@ -234,6 +235,93 @@ function validateMetaModel(input) { return serializer.toJSON(object); } +/** + * Create a name resolution table + * @param {*} modelManager - the model manager + * @param {object} metaModel - the metamodel (JSON) + * @return {object} mapping from local to fully qualified names + */ +function createNameTable(modelManager, metaModel) { + const table = {}; + + // First list the imported names in order (overriding as we go along) + const imports = metaModel.imports; + imports.forEach((imp) => { + const namespace = imp.namespace; + const modelFile = modelManager.getModelFile(namespace); + if (imp.$class === 'concerto.metamodel.ImportType') { + if (!modelFile.getLocalType(imp.name)) { + throw new Error(`Declaration ${imp.identifier.name} in namespace ${namespace} not found`); + } + table[imp.name] = namespace; + } else { + const decls = modelFile.getAllDeclarations(); + decls.forEach((decl) => { + table[decl.getName()] = namespace; + }); + } + }); + + // Then add the names local to this metaModel (overriding as we go along) + metaModel.declarations.forEach((decl) => { + table[decl.name] = metaModel.namespace; + }); + + return table; +} + +/** + * Resolve a name using the name table + * @param {string} name - the name of the type to resolve + * @param {object} table - the name table + * @return {string} the fully qualified name + */ +function resolveName(name, table) { + if (!table[name]) { + throw new Error(`Name ${name} not found`); + } + const namespace = table[name]; + return `${namespace}.${name}`; +} + +/** + * Name resolution for metamodel + * @param {object} metaModel - the metamodel (JSON) + * @param {object} table - the name table + * @return {object} the metamodel with fully qualified names + */ +function resolveTypeNames(metaModel, table) { + switch (metaModel.$class) { + case 'concerto.metamodel.ModelFile': { + metaModel.declarations.forEach((decl) => { + resolveTypeNames(decl, table); + }); + } + break; + case 'concerto.metamodel.AssetDeclaration': + case 'concerto.metamodel.ConceptDeclaration': + case 'concerto.metamodel.EventDeclaration': + case 'concerto.metamodel.TransactionDeclaration': + case 'concerto.metamodel.ParticipantDeclaration': { + if (metaModel.superType) { + const name = metaModel.superType.name; + metaModel.superType.fullyQualifiedName = resolveName(name, table); + } + metaModel.fields.forEach((field) => { + resolveTypeNames(field, table); + }); + } + break; + case 'concerto.metamodel.ObjectFieldDeclaration': + case 'concerto.metamodel.RelationshipDeclaration': { + const name = metaModel.type.name; + metaModel.type.fullyQualifiedName = resolveName(name, table); + } + break; + } + return metaModel; +} + /** * Create metamodel for a field * @param {object} ast - the AST for the field @@ -468,10 +556,7 @@ function modelToMetaModel(ast, validate = true) { ns.$class = 'concerto.metamodel.ImportAll'; } else { ns.$class = 'concerto.metamodel.ImportType'; - ns.identifier = { - $class: 'concerto.metamodel.TypeIdentifier', - name - }; + ns.name = name; } if(imp.uri) { ns.uri = imp.uri; @@ -647,7 +732,7 @@ function ctoFromMetaModel(metaModel, validate = true) { mm.imports.forEach((imp) => { let name = '*'; if (imp.$class === 'concerto.metamodel.ImportType') { - name = imp.identifier.name; + name = imp.name; } result += `\nimport ${imp.namespace}.${name}`; if (imp.uri) { @@ -665,7 +750,7 @@ function ctoFromMetaModel(metaModel, validate = true) { /** * Export metamodel from a model string - * @param {object} model - the string for the model + * @param {string} model - the string for the model * @param {boolean} [validate] - whether to perform validation * @return {object} the metamodel for this model */ @@ -674,9 +759,26 @@ function ctoToMetaModel(model, validate) { return modelToMetaModel(ast); } +/** + * Export metamodel from a model string and resolve names + * @param {*} modelManager - the model manager + * @param {string} model - the string for the model + * @param {boolean} [validate] - whether to perform validation + * @return {object} the metamodel for this model + */ +function ctoToMetaModelAndResolve(modelManager, model, validate) { + const ast = parser.parse(model); + const metaModel = modelToMetaModel(ast); + const nameTable = createNameTable(modelManager, metaModel); + // This adds the fully qualified names to the same object + resolveTypeNames(metaModel, nameTable); + return metaModel; +} + module.exports = { metaModelCto, modelFileToMetaModel, ctoToMetaModel, + ctoToMetaModelAndResolve, ctoFromMetaModel, }; diff --git a/packages/concerto-core/test/data/model/person.cto b/packages/concerto-core/test/data/model/person.cto index a48c6b1deb..e97a1adfb9 100644 --- a/packages/concerto-core/test/data/model/person.cto +++ b/packages/concerto-core/test/data/model/person.cto @@ -70,4 +70,6 @@ event E { transaction T identified by id { o String id + o TemporalUnit tu + o Month month // This is imported from the org.accordproject.time namespace } diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index ea80a8fd8d..bd1fbe044d 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -9,10 +9,7 @@ }, { "$class": "concerto.metamodel.ImportType", - "identifier": { - "$class": "concerto.metamodel.TypeIdentifier", - "name": "TemporalUnit" - }, + "name": "TemporalUnit", "namespace": "org.accordproject.time", "uri": "https://models.accordproject.org/time@0.2.0.cto" } @@ -282,6 +279,26 @@ "name": "id", "isArray": false, "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "TemporalUnit" + }, + "name": "tu", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Month" + }, + "name": "month", + "isArray": false, + "isOptional": false } ] } diff --git a/packages/concerto-core/test/data/model/personResolved.json b/packages/concerto-core/test/data/model/personResolved.json new file mode 100644 index 0000000000..979ad27fcb --- /dev/null +++ b/packages/concerto-core/test/data/model/personResolved.json @@ -0,0 +1,317 @@ +{ + "$class": "concerto.metamodel.ModelFile", + "namespace": "test.person", + "imports": [ + { + "$class": "concerto.metamodel.ImportAll", + "namespace": "org.accordproject.time", + "uri": "https://models.accordproject.org/time@0.2.0.cto" + }, + { + "$class": "concerto.metamodel.ImportType", + "name": "TemporalUnit", + "namespace": "org.accordproject.time", + "uri": "https://models.accordproject.org/time@0.2.0.cto" + } + ], + "declarations": [ + { + "$class": "concerto.metamodel.EnumDeclaration", + "isAbstract": false, + "name": "Gender", + "fields": [ + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "MALE", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "FEMALE", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.EnumFieldDeclaration", + "name": "OTHER", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "isAbstract": true, + "name": "Individual", + "fields": [] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "isAbstract": false, + "name": "Person", + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Individual", + "fullyQualifiedName": "test.person.Individual" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "firstName", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "lastName", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "name": "address", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "defaultValue": "USAddress", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "name": "address2", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "name": "address3", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "name": "address4", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel.RelationshipDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "name": "address5", + "isArray": false, + "isOptional": true + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Gender", + "fullyQualifiedName": "test.person.Gender" + }, + "name": "gender", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DateTimeFieldDeclaration", + "name": "dob", + "isArray": false, + "isOptional": true + } + ] + }, + { + "$class": "concerto.metamodel.ParticipantDeclaration", + "isAbstract": false, + "name": "Employee", + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Person", + "fullyQualifiedName": "test.person.Person" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "company", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.BooleanFieldDeclaration", + "name": "onLeave", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ConceptDeclaration", + "isAbstract": false, + "name": "Address", + "identified": { + "$class": "concerto.metamodel.Identified" + }, + "fields": [ + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "defaultValue": 10001, + "name": "zip", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "defaultValue": 10001, + "name": "zip2", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "defaultValue": 10001, + "name": "zip3", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "defaultValue": "NYC", + "name": "city", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "defaultValue": "USA", + "name": "country", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "street", + "isArray": true, + "isOptional": false + }, + { + "$class": "concerto.metamodel.BooleanFieldDeclaration", + "defaultValue": true, + "name": "isResidential", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.BooleanFieldDeclaration", + "defaultValue": false, + "name": "isPrivate", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.ConceptDeclaration", + "isAbstract": false, + "name": "USAddress", + "superType": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Address", + "fullyQualifiedName": "test.person.Address" + }, + "fields": [ + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "name": "zip4", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "name": "zip5", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "name": "zip6", + "isArray": false, + "isOptional": false + } + ] + }, + { + "$class": "concerto.metamodel.AssetDeclaration", + "isAbstract": false, + "name": "A", + "fields": [] + }, + { + "$class": "concerto.metamodel.EventDeclaration", + "isAbstract": false, + "name": "E", + "fields": [] + }, + { + "$class": "concerto.metamodel.TransactionDeclaration", + "isAbstract": false, + "name": "T", + "identified": { + "$class": "concerto.metamodel.IdentifiedBy", + "name": "id" + }, + "fields": [ + { + "$class": "concerto.metamodel.StringFieldDeclaration", + "name": "id", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "TemporalUnit", + "fullyQualifiedName": "org.accordproject.time.TemporalUnit" + }, + "name": "tu", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.ObjectFieldDeclaration", + "type": { + "$class": "concerto.metamodel.TypeIdentifier", + "name": "Month", + "fullyQualifiedName": "org.accordproject.time.Month" + }, + "name": "month", + "isArray": false, + "isOptional": false + } + ] + } + ] +} diff --git a/packages/concerto-core/test/introspect/metamodel.js b/packages/concerto-core/test/introspect/metamodel.js index 484938860b..e2b7d873be 100644 --- a/packages/concerto-core/test/introspect/metamodel.js +++ b/packages/concerto-core/test/introspect/metamodel.js @@ -15,6 +15,7 @@ 'use strict'; const ModelManager = require('../../lib/modelmanager'); +const ModelLoader = require('../../lib/modelloader'); const ModelFile = require('../../lib/introspect/modelfile'); const MetaModel = require('../../lib/introspect/metamodel'); const fs = require('fs'); @@ -25,8 +26,10 @@ chai.should(); chai.use(require('chai-things')); describe('MetaModel', () => { - const personModel = fs.readFileSync(path.resolve(__dirname, '../data/model/person.cto'), 'utf8'); + const personModelPath = path.resolve(__dirname, '../data/model/person.cto'); + const personModel = fs.readFileSync(personModelPath, 'utf8'); const personMetaModel = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/model/person.json'), 'utf8')); + const personMetaModelResolved = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/model/personResolved.json'), 'utf8')); describe('#toMetaModel', () => { it('should convert a CTO model to its metamodel', () => { @@ -39,6 +42,12 @@ describe('MetaModel', () => { mm1.should.deep.equal(personMetaModel); }); + it('should convert and validate a CTO model to its metamodel with name resolution', async () => { + const modelManager = await ModelLoader.loadModelManager([personModelPath]); + const mm1 = MetaModel.ctoToMetaModelAndResolve(modelManager, personModel); + mm1.should.deep.equal(personMetaModelResolved); + }); + it('should convert a ModelFile to its metamodel', () => { const modelManager1 = new ModelManager(); const mf1 = new ModelFile(modelManager1, personModel); diff --git a/packages/concerto-core/test/modelloader.js b/packages/concerto-core/test/modelloader.js index 5f9b2c931a..d7d1bf7386 100644 --- a/packages/concerto-core/test/modelloader.js +++ b/packages/concerto-core/test/modelloader.js @@ -27,8 +27,8 @@ chai.use(require('chai-as-promised')); describe('ModelLoader', () => { - let modelBase = __dirname + '/data/model/model-base.cto'; - let modelUrl = 'https://models.accordproject.org/patents/patent.cto'; + const modelBase = __dirname + '/data/model/model-base.cto'; + const modelUrl = 'https://models.accordproject.org/patents/patent.cto'; beforeEach(() => { }); From 8445835e2f0cf01b036e65b7ad3d582fac3bc856 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 26 Jul 2021 16:44:26 -0400 Subject: [PATCH 08/17] fix(core) Update the TypeScript interface Signed-off-by: jeromesimeon --- packages/concerto-core/types/index.d.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/concerto-core/types/index.d.ts b/packages/concerto-core/types/index.d.ts index c95f0df3c4..1e95c1b5c0 100644 --- a/packages/concerto-core/types/index.d.ts +++ b/packages/concerto-core/types/index.d.ts @@ -451,9 +451,10 @@ declare module '@accordproject/concerto-core' { // MetaModel export namespace MetaModel { const metaModelCto: string; - function modelFileToMetaModel(modelFile: ModelFile, validate: boolean): any; - function ctoToMetaModel(model: string, validate: boolean): any; - function ctoFromMetaModel(metaModel: any, validate: boolean): string; + function modelFileToMetaModel(modelFile: ModelFile, validate?: boolean): any; + function ctoToMetaModel(model: string, validate?: boolean): any; + function ctoToMetaModelAndResolve(modelManager: ModelManager, model: string, validate?: boolean); + function ctoFromMetaModel(metaModel: any, validate?: boolean): string; } // version From a4c63c13df81f72188449d165bc6e0333c980023 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 26 Jul 2021 17:16:26 -0400 Subject: [PATCH 09/17] fix(metamodel) Hide fullyQualifiedName annotation for the form builder Signed-off-by: jeromesimeon --- packages/concerto-core/lib/introspect/metamodel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index 3208493e1b..eba6959461 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -56,6 +56,7 @@ concept DecoratorBoolean extends DecoratorLiteral { concept TypeIdentifier { @FormEditor("selectOptions", "types") o String name default="Concept" + @FormEditor( "hide", true) o String fullyQualifiedName optional } From f3ccc4b4b253258cbfc243850552512125c0e69d Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 28 Jul 2021 09:21:17 -0400 Subject: [PATCH 10/17] fix(metamodel) Distinguish between class declarations and enum declarations Signed-off-by: jeromesimeon --- packages/concerto-core/api.txt | 4 +- packages/concerto-core/changelog.txt | 2 +- .../concerto-core/lib/introspect/metamodel.js | 105 ++++++++++++------ .../concerto-core/test/data/model/person.json | 49 ++++---- .../test/data/model/personResolved.json | 49 ++++---- 5 files changed, 118 insertions(+), 91 deletions(-) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index aad6734e46..9790c85142 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -114,9 +114,11 @@ class ModelFileDownloader { + object createNameTable() + string resolveName() + object resolveTypeNames() + + object enumFieldToMetaModel() + object fieldToMetaModel() + object relationshipToMetaModel() - + object enumPropertyToMetaModel() + + object enumDeclToMetaModel() + + object classDeclToMetaModel() + object declToMetaModel() + object modelToMetaModel() + object modelFileToMetaModel() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index bc9c8248ae..31da00f99b 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 {e441f02829c80dd1b4409c71c8ddd28f} 2021-07-13 +Version 1.0.5 {1af0901bd065706dc1409d1e3482fa48} 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 eba6959461..0e389427fa 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -77,14 +77,30 @@ concept IdentifiedBy extends Identified { o String name } +abstract concept Declaration { + // 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)*$/ +} + +concept EnumDeclaration extends Declaration { + o EnumFieldDeclaration[] fields +} + +concept EnumFieldDeclaration { + // 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("hide", true) + o Decorator[] decorators optional +} + @FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") -abstract concept ClassDeclaration { +abstract concept ClassDeclaration extends Declaration { @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 @@ -106,9 +122,6 @@ concept EventDeclaration extends ClassDeclaration { concept ConceptDeclaration extends ClassDeclaration { } -concept EnumDeclaration extends ClassDeclaration { -} - @FormEditor("defaultSubclass","concerto.metamodel.StringFieldDeclaration") abstract concept FieldDeclaration { // TODO Allow regex modifiers e.g. //ui @@ -123,9 +136,6 @@ abstract concept FieldDeclaration { o Decorator[] decorators optional } -concept EnumFieldDeclaration extends FieldDeclaration { -} - concept RelationshipDeclaration extends FieldDeclaration { @FormEditor("title", "Type Name", "selectOptions", "types") o TypeIdentifier type @@ -208,7 +218,7 @@ concept ModelFile { @FormEditor("hide", true) o Import[] imports optional @FormEditor("title", "Classes") - o ClassDeclaration[] declarations optional + o Declaration[] declarations optional } `; @@ -324,7 +334,24 @@ function resolveTypeNames(metaModel, table) { } /** - * Create metamodel for a field + * Create metamodel for an enum field + * @param {object} ast - the AST for the field + * @return {object} the metamodel for this field + */ +function enumFieldToMetaModel(ast) { + // console.log(`FIELD ${JSON.stringify(ast)}`); + const field = {}; + + field.$class = 'concerto.metamodel.EnumFieldDeclaration'; + + // Field name + field.name = ast.id.name; + + return field; +} + +/** + * Create metamodel for a class field * @param {object} ast - the AST for the field * @return {object} the metamodel for this field */ @@ -434,31 +461,35 @@ function relationshipToMetaModel(ast) { } /** - * Create metamodel for an enum field - * @param {object} ast - the AST for the enum field - * @return {object} the metamodel for this enum field + * Create metamodel for an enum declaration + * @param {object} ast - the AST for the enum declaration + * @return {object} the metamodel for this enum declaration */ -function enumPropertyToMetaModel(ast) { - let property = { - $class: 'concerto.metamodel.EnumFieldDeclaration', - }; +function enumDeclToMetaModel(ast) { + let decl = {}; - // Field name - property.name = ast.id.name; - // Is it an array? - property.isArray = false; - // Is it an optional? - property.isOptional = false; + decl.$class = 'concerto.metamodel.EnumDeclaration'; + + // The enum name + decl.name = ast.id.name; - return property; + // Enum fields + decl.fields = []; + for (let n = 0; n < ast.body.declarations.length; n++) { + let thing = ast.body.declarations[n]; + + decl.fields.push(enumFieldToMetaModel(thing)); + } + + return decl; } /** * Create metamodel for a class declaration - * @param {object} ast - the AST for the declaration - * @return {object} the metamodel for this declaration + * @param {object} ast - the AST for the class declaration + * @return {object} the metamodel for this class declaration */ -function declToMetaModel(ast) { +function classDeclToMetaModel(ast) { let decl = {}; if(ast.type === 'AssetDeclaration') { @@ -471,8 +502,6 @@ function declToMetaModel(ast) { decl.$class = 'concerto.metamodel.ParticipantDeclaration'; } else if (ast.type === 'TransactionDeclaration') { decl.$class = 'concerto.metamodel.TransactionDeclaration'; - } else if (ast.type === 'EnumDeclaration') { - decl.$class = 'concerto.metamodel.EnumDeclaration'; } // The class name @@ -523,14 +552,24 @@ function declToMetaModel(ast) { decl.fields.push(fieldToMetaModel(thing)); } else if (thing.type === 'RelationshipDeclaration') { decl.fields.push(relationshipToMetaModel(thing)); - } else if (thing.type === 'EnumPropertyDeclaration') { - decl.fields.push(enumPropertyToMetaModel(thing)); } } return decl; } +/** + * Create metamodel for a declaration + * @param {object} ast - the AST for the declaration + * @return {object} the metamodel for this declaration + */ +function declToMetaModel(ast) { + if(ast.type === 'EnumDeclaration') { + return enumDeclToMetaModel(ast); + } + return classDeclToMetaModel(ast); +} + /** * Export metamodel from an AST * @param {object} ast - the AST for the model diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index bd1fbe044d..53bfe72d7a 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -17,39 +17,31 @@ "declarations": [ { "$class": "concerto.metamodel.EnumDeclaration", - "isAbstract": false, - "name": "Gender", "fields": [ { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "MALE", - "isArray": false, - "isOptional": false + "name": "MALE" }, { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "FEMALE", - "isArray": false, - "isOptional": false + "name": "FEMALE" }, { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "OTHER", - "isArray": false, - "isOptional": false + "name": "OTHER" } - ] + ], + "name": "Gender" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": true, - "name": "Individual", - "fields": [] + "fields": [], + "name": "Individual" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": false, - "name": "Person", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Individual" @@ -134,12 +126,12 @@ "isArray": false, "isOptional": true } - ] + ], + "name": "Person" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": false, - "name": "Employee", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Person" @@ -157,12 +149,12 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "Employee" }, { "$class": "concerto.metamodel.ConceptDeclaration", "isAbstract": false, - "name": "Address", "identified": { "$class": "concerto.metamodel.Identified" }, @@ -222,12 +214,12 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "Address" }, { "$class": "concerto.metamodel.ConceptDeclaration", "isAbstract": false, - "name": "USAddress", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Address" @@ -251,24 +243,24 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "USAddress" }, { "$class": "concerto.metamodel.AssetDeclaration", "isAbstract": false, - "name": "A", - "fields": [] + "fields": [], + "name": "A" }, { "$class": "concerto.metamodel.EventDeclaration", "isAbstract": false, - "name": "E", - "fields": [] + "fields": [], + "name": "E" }, { "$class": "concerto.metamodel.TransactionDeclaration", "isAbstract": false, - "name": "T", "identified": { "$class": "concerto.metamodel.IdentifiedBy", "name": "id" @@ -300,7 +292,8 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "T" } ] } diff --git a/packages/concerto-core/test/data/model/personResolved.json b/packages/concerto-core/test/data/model/personResolved.json index 979ad27fcb..64bb5f3ab1 100644 --- a/packages/concerto-core/test/data/model/personResolved.json +++ b/packages/concerto-core/test/data/model/personResolved.json @@ -17,39 +17,31 @@ "declarations": [ { "$class": "concerto.metamodel.EnumDeclaration", - "isAbstract": false, - "name": "Gender", "fields": [ { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "MALE", - "isArray": false, - "isOptional": false + "name": "MALE" }, { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "FEMALE", - "isArray": false, - "isOptional": false + "name": "FEMALE" }, { "$class": "concerto.metamodel.EnumFieldDeclaration", - "name": "OTHER", - "isArray": false, - "isOptional": false + "name": "OTHER" } - ] + ], + "name": "Gender" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": true, - "name": "Individual", - "fields": [] + "fields": [], + "name": "Individual" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": false, - "name": "Person", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Individual", @@ -141,12 +133,12 @@ "isArray": false, "isOptional": true } - ] + ], + "name": "Person" }, { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": false, - "name": "Employee", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Person", @@ -165,12 +157,12 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "Employee" }, { "$class": "concerto.metamodel.ConceptDeclaration", "isAbstract": false, - "name": "Address", "identified": { "$class": "concerto.metamodel.Identified" }, @@ -230,12 +222,12 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "Address" }, { "$class": "concerto.metamodel.ConceptDeclaration", "isAbstract": false, - "name": "USAddress", "superType": { "$class": "concerto.metamodel.TypeIdentifier", "name": "Address", @@ -260,24 +252,24 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "USAddress" }, { "$class": "concerto.metamodel.AssetDeclaration", "isAbstract": false, - "name": "A", - "fields": [] + "fields": [], + "name": "A" }, { "$class": "concerto.metamodel.EventDeclaration", "isAbstract": false, - "name": "E", - "fields": [] + "fields": [], + "name": "E" }, { "$class": "concerto.metamodel.TransactionDeclaration", "isAbstract": false, - "name": "T", "identified": { "$class": "concerto.metamodel.IdentifiedBy", "name": "id" @@ -311,7 +303,8 @@ "isArray": false, "isOptional": false } - ] + ], + "name": "T" } ] } From 82d679630a6ea8ed85a51ba39a2621fc3601ce76 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 28 Jul 2021 10:00:19 -0400 Subject: [PATCH 11/17] feature(cli) Always include imported model for name resolution Signed-off-by: jeromesimeon --- packages/concerto-cli/lib/commands.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index 691bfd4f8d..6a87de210c 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -189,11 +189,15 @@ class Commands { * Import a CTO string to its metamodel * * @param {string} input - CTO - * @param {string[]} ctoFiles - the CTO files used for import resolution + * @param {string[]} [ctoFiles] - the CTO files used for import resolution * @param {boolean} resolve - whether to resolve the names * @param {string} the metamodel */ - static async import(input, ctoFiles, resolve) { + static async import(input, ctoFiles = [], resolve) { + // Add input to ctoFiles for convenience + if (!ctoFiles.includes(input)) { + ctoFiles.push(input); + } const inputString = fs.readFileSync(input, 'utf8'); let result; if (resolve) { From 0c1229d6dde6b4fd0a7c42eb79137894deeccf71 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 28 Jul 2021 10:00:44 -0400 Subject: [PATCH 12/17] fix(metamodel) More explicit separation between enums and classes Signed-off-by: jeromesimeon --- .../concerto-cli/test/models/contract.json | 3 +- .../test/models/contractResolved.json | 3 +- .../concerto-core/lib/introspect/metamodel.js | 33 +++++++++++++++---- .../concerto-core/test/data/model/person.json | 6 ++-- .../test/data/model/personResolved.json | 6 ++-- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/concerto-cli/test/models/contract.json b/packages/concerto-cli/test/models/contract.json index ba618f738f..dcb74414aa 100644 --- a/packages/concerto-cli/test/models/contract.json +++ b/packages/concerto-cli/test/models/contract.json @@ -2,7 +2,8 @@ "$class": "concerto.metamodel.ModelFile", "namespace": "org.accordproject.cicero.contract", "imports": [], - "declarations": [ + "enumDeclarations": [], + "classDeclarations": [ { "$class": "concerto.metamodel.AssetDeclaration", "name": "AccordContractState", diff --git a/packages/concerto-cli/test/models/contractResolved.json b/packages/concerto-cli/test/models/contractResolved.json index 069e40bc52..9d640bd4e7 100644 --- a/packages/concerto-cli/test/models/contractResolved.json +++ b/packages/concerto-cli/test/models/contractResolved.json @@ -2,7 +2,8 @@ "$class": "concerto.metamodel.ModelFile", "namespace": "org.accordproject.cicero.contract", "imports": [], - "declarations": [ + "enumDeclarations": [], + "classDeclarations": [ { "$class": "concerto.metamodel.AssetDeclaration", "isAbstract": false, diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index 0e389427fa..d187a9901f 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -77,6 +77,7 @@ concept IdentifiedBy extends Identified { o String name } +@FormEditor("defaultSubclass","concerto.metamodel.ClassDeclaration") abstract concept Declaration { // 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") @@ -217,8 +218,10 @@ concept ModelFile { o String namespace default="my.namespace" @FormEditor("hide", true) o Import[] imports optional + @FormEditor("title", "Enums") + o EnumDeclaration[] enumDeclarations optional @FormEditor("title", "Classes") - o Declaration[] declarations optional + o ClassDeclaration[] classDeclarations optional } `; @@ -274,7 +277,10 @@ function createNameTable(modelManager, metaModel) { }); // Then add the names local to this metaModel (overriding as we go along) - metaModel.declarations.forEach((decl) => { + metaModel.enumDeclarations.forEach((decl) => { + table[decl.name] = metaModel.namespace; + }); + metaModel.classDeclarations.forEach((decl) => { table[decl.name] = metaModel.namespace; }); @@ -304,7 +310,10 @@ function resolveName(name, table) { function resolveTypeNames(metaModel, table) { switch (metaModel.$class) { case 'concerto.metamodel.ModelFile': { - metaModel.declarations.forEach((decl) => { + metaModel.enumDeclarations.forEach((decl) => { + resolveTypeNames(decl, table); + }); + metaModel.classDeclarations.forEach((decl) => { resolveTypeNames(decl, table); }); } @@ -606,12 +615,17 @@ function modelToMetaModel(ast, validate = true) { } if (ast.body.length > 0) { - metamodel.declarations = []; + metamodel.enumDeclarations = []; + metamodel.classDeclarations = []; } for(let n=0; n < ast.body.length; n++ ) { const thing = ast.body[n]; const decl = declToMetaModel(thing); - metamodel.declarations.push(decl); + if (decl.$class === 'concerto.metamodel.EnumDeclaration') { + metamodel.enumDeclarations.push(decl); + } else { + metamodel.classDeclarations.push(decl); + } } // Last, validate the JSON metaModel @@ -780,8 +794,13 @@ function ctoFromMetaModel(metaModel, validate = true) { } }); } - if (mm.declarations && mm.declarations.length > 0) { - mm.declarations.forEach((decl) => { + if (mm.enumDeclarations && mm.enumDeclarations.length > 0) { + mm.enumDeclarations.forEach((decl) => { + result += `\n\n${declFromMetaModel(decl)}`; + }); + } + if (mm.classDeclarations && mm.classDeclarations.length > 0) { + mm.classDeclarations.forEach((decl) => { result += `\n\n${declFromMetaModel(decl)}`; }); } diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index 53bfe72d7a..3977c7ded8 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -14,7 +14,7 @@ "uri": "https://models.accordproject.org/time@0.2.0.cto" } ], - "declarations": [ + "enumDeclarations": [ { "$class": "concerto.metamodel.EnumDeclaration", "fields": [ @@ -32,7 +32,9 @@ } ], "name": "Gender" - }, + } + ], + "classDeclarations": [ { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": true, diff --git a/packages/concerto-core/test/data/model/personResolved.json b/packages/concerto-core/test/data/model/personResolved.json index 64bb5f3ab1..6bfb937823 100644 --- a/packages/concerto-core/test/data/model/personResolved.json +++ b/packages/concerto-core/test/data/model/personResolved.json @@ -14,7 +14,7 @@ "uri": "https://models.accordproject.org/time@0.2.0.cto" } ], - "declarations": [ + "enumDeclarations": [ { "$class": "concerto.metamodel.EnumDeclaration", "fields": [ @@ -32,7 +32,9 @@ } ], "name": "Gender" - }, + } + ], + "classDeclarations": [ { "$class": "concerto.metamodel.ParticipantDeclaration", "isAbstract": true, From a724127c9ba2fefbad6f7a6edab1c7a52f7658c6 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 28 Jul 2021 10:31:36 -0400 Subject: [PATCH 13/17] fix(metamodel) Some adjustments for the metamodel Signed-off-by: jeromesimeon --- packages/concerto-core/lib/introspect/metamodel.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index d187a9901f..ff8a35cefe 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -77,14 +77,10 @@ concept IdentifiedBy extends Identified { o String name } -@FormEditor("defaultSubclass","concerto.metamodel.ClassDeclaration") -abstract concept Declaration { +concept EnumDeclaration { // 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") + @FormEditor("title", "Enum 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)*$/ -} - -concept EnumDeclaration extends Declaration { o EnumFieldDeclaration[] fields } @@ -98,7 +94,10 @@ concept EnumFieldDeclaration { } @FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") -abstract concept ClassDeclaration extends Declaration { +abstract concept ClassDeclaration { + // 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", "Class 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)*$/ @FormEditor("hide", true) o Decorator[] decorators optional o Boolean isAbstract default=false From c0025508d04e6cef9099ff3fdfd37472b94e1383 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 4 Aug 2021 08:37:15 -0400 Subject: [PATCH 14/17] fix(metamodel) Add regular expressions for names back Signed-off-by: jeromesimeon --- .../concerto-core/lib/introspect/metamodel.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index ff8a35cefe..ba60150def 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -42,7 +42,7 @@ abstract concept DecoratorLiteral { } concept DecoratorString extends DecoratorLiteral { - o String value + o String value } concept DecoratorNumber extends DecoratorLiteral { @@ -78,26 +78,21 @@ concept IdentifiedBy extends Identified { } concept EnumDeclaration { - // 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", "Enum 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 String name default="ClassName" 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 o EnumFieldDeclaration[] fields } concept EnumFieldDeclaration { - // 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)*$/ + o String name default="fieldName" 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("hide", true) o Decorator[] decorators optional } @FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") abstract concept ClassDeclaration { - // 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", "Class 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 String name default="ClassName" 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("hide", true) o Decorator[] decorators optional o Boolean isAbstract default=false @@ -124,10 +119,7 @@ concept ConceptDeclaration extends ClassDeclaration { @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)*$/ + o String name default="fieldName" 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", "Is Array?") o Boolean isArray default=false @FormEditor("title", "Is Optional?") From 76f983764411aacc2f70521f27bbea05739f9280 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 4 Aug 2021 09:52:59 -0400 Subject: [PATCH 15/17] feature(metamodel) Add support for validators (range and regex) Signed-off-by: jeromesimeon --- .../concerto-core/lib/introspect/metamodel.js | 65 ++++++++++ .../concerto-core/test/data/model/person.cto | 14 ++- .../concerto-core/test/data/model/person.json | 115 +++++++++++++++--- .../test/data/model/personResolved.json | 115 +++++++++++++++--- 4 files changed, 269 insertions(+), 40 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index ba60150def..ba4744c321 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -381,18 +381,54 @@ function fieldToMetaModel(ast) { if (ast.default) { field.defaultValue = parseInt(ast.default); } + if (ast.range) { + const validator = { + $class: 'concerto.metamodel.IntegerDomainValidator', + }; + if (ast.range.lower) { + validator.lower = parseInt(ast.range.lower); + } + if (ast.range.upper) { + validator.upper = parseInt(ast.range.upper); + } + field.validator = validator; + } break; case 'Long': field.$class = 'concerto.metamodel.LongFieldDeclaration'; if (ast.default) { field.defaultValue = parseInt(ast.default); } + if (ast.range) { + const validator = { + $class: 'concerto.metamodel.LongDomainValidator', + }; + if (ast.range.lower) { + validator.lower = parseInt(ast.range.lower); + } + if (ast.range.upper) { + validator.upper = parseInt(ast.range.upper); + } + field.validator = validator; + } break; case 'Double': field.$class = 'concerto.metamodel.DoubleFieldDeclaration'; if (ast.default) { field.defaultValue = parseFloat(ast.default); } + if (ast.range) { + const validator = { + $class: 'concerto.metamodel.DoubleDomainValidator', + }; + if (ast.range.lower) { + validator.lower = parseFloat(ast.range.lower); + } + if (ast.range.upper) { + validator.upper = parseFloat(ast.range.upper); + } + field.validator = validator; + } break; case 'Boolean': field.$class = 'concerto.metamodel.BooleanFieldDeclaration'; @@ -412,6 +448,13 @@ function fieldToMetaModel(ast) { if (ast.default) { field.defaultValue = ast.default; } + if (ast.regex) { + const regex = ast.regex.flags ? `/${ast.regex.pattern}/${ast.regex.flags}` : `/${ast.regex.pattern}/}`; + field.validator = { + $class: 'concerto.metamodel.StringRegexValidator', + regex, + }; + } break; default: field.$class = 'concerto.metamodel.ObjectFieldDeclaration'; @@ -643,11 +686,14 @@ function modelFileToMetaModel(modelFile, validate) { function fieldFromMetaModel(mm) { let result = ''; let defaultString = ''; + let validatorString = ''; + if (mm.$class === 'concerto.metamodel.RelationshipDeclaration') { result += '-->'; } else { result += 'o'; } + switch (mm.$class) { case 'concerto.metamodel.EnumFieldDeclaration': break; @@ -671,24 +717,42 @@ function fieldFromMetaModel(mm) { defaultString += ` default=${doubleString}`; } + if (mm.validator) { + const lowerString = mm.validator.lower ? mm.validator.lower : ''; + const upperString = mm.validator.upper ? mm.validator.upper : ''; + validatorString += ` range=[${lowerString},${upperString}]`; + } break; case 'concerto.metamodel.IntegerFieldDeclaration': result += ' Integer'; if (mm.defaultValue) { defaultString += ` default=${mm.defaultValue.toString()}`; } + if (mm.validator) { + const lowerString = mm.validator.lower ? mm.validator.lower : ''; + const upperString = mm.validator.upper ? mm.validator.upper : ''; + validatorString += ` range=[${lowerString},${upperString}]`; + } break; case 'concerto.metamodel.LongFieldDeclaration': result += ' Long'; if (mm.defaultValue) { defaultString += ` default=${mm.defaultValue.toString()}`; } + if (mm.validator) { + const lowerString = mm.validator.lower ? mm.validator.lower : ''; + const upperString = mm.validator.upper ? mm.validator.upper : ''; + validatorString += ` range=[${lowerString},${upperString}]`; + } break; case 'concerto.metamodel.StringFieldDeclaration': result += ' String'; if (mm.defaultValue) { defaultString += ` default="${mm.defaultValue}"`; } + if (mm.validator) { + validatorString += ` regex=${mm.validator.regex}`; + } break; case 'concerto.metamodel.ObjectFieldDeclaration': result += ` ${mm.type.name}`; @@ -708,6 +772,7 @@ function fieldFromMetaModel(mm) { result += ' optional'; } result += defaultString; + result += validatorString; return result; } diff --git a/packages/concerto-core/test/data/model/person.cto b/packages/concerto-core/test/data/model/person.cto index e97a1adfb9..3a5ee5e069 100644 --- a/packages/concerto-core/test/data/model/person.cto +++ b/packages/concerto-core/test/data/model/person.cto @@ -29,7 +29,7 @@ abstract participant Individual { } participant Person extends Individual { - o String firstName + o String firstName regex=/[a-zA-Z]*/u o String lastName o Address address o Address address2 default="USAddress" @@ -57,9 +57,15 @@ concept Address identified { } concept USAddress extends Address { - o Integer zip4 - o Long zip5 - o Double zip6 + o Integer zip4 range=[-365,365] + o Integer zip41 range=[,365] + o Integer zip42 range=[-365,] + o Long zip5 range=[-365,365] + o Long zip51 range=[,365] + o Long zip52 range=[-365,] + o Double zip6 range=[-3.14,3.14] + o Double zip61 range=[-3.14,] + o Double zip62 range=[,3.14] } asset A { diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index 3977c7ded8..7b4082270c 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -17,6 +17,7 @@ "enumDeclarations": [ { "$class": "concerto.metamodel.EnumDeclaration", + "name": "Gender", "fields": [ { "$class": "concerto.metamodel.EnumFieldDeclaration", @@ -30,19 +31,19 @@ "$class": "concerto.metamodel.EnumFieldDeclaration", "name": "OTHER" } - ], - "name": "Gender" + ] } ], "classDeclarations": [ { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Individual", "isAbstract": true, - "fields": [], - "name": "Individual" + "fields": [] }, { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Person", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -51,6 +52,10 @@ "fields": [ { "$class": "concerto.metamodel.StringFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.StringRegexValidator", + "regex": "/[a-zA-Z]*/u" + }, "name": "firstName", "isArray": false, "isOptional": false @@ -128,11 +133,11 @@ "isArray": false, "isOptional": true } - ], - "name": "Person" + ] }, { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Employee", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -151,11 +156,11 @@ "isArray": false, "isOptional": false } - ], - "name": "Employee" + ] }, { "$class": "concerto.metamodel.ConceptDeclaration", + "name": "Address", "isAbstract": false, "identified": { "$class": "concerto.metamodel.Identified" @@ -216,11 +221,11 @@ "isArray": false, "isOptional": false } - ], - "name": "Address" + ] }, { "$class": "concerto.metamodel.ConceptDeclaration", + "name": "USAddress", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -229,39 +234,114 @@ "fields": [ { "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "lower": -365, + "upper": 365 + }, "name": "zip4", "isArray": false, "isOptional": false }, + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "upper": 365 + }, + "name": "zip41", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "lower": -365 + }, + "name": "zip42", + "isArray": false, + "isOptional": false + }, { "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "lower": -365, + "upper": 365 + }, "name": "zip5", "isArray": false, "isOptional": false }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "upper": 365 + }, + "name": "zip51", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "lower": -365 + }, + "name": "zip52", + "isArray": false, + "isOptional": false + }, { "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "lower": -3.14, + "upper": 3.14 + }, "name": "zip6", "isArray": false, "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "lower": -3.14 + }, + "name": "zip61", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "upper": 3.14 + }, + "name": "zip62", + "isArray": false, + "isOptional": false } - ], - "name": "USAddress" + ] }, { "$class": "concerto.metamodel.AssetDeclaration", + "name": "A", "isAbstract": false, - "fields": [], - "name": "A" + "fields": [] }, { "$class": "concerto.metamodel.EventDeclaration", + "name": "E", "isAbstract": false, - "fields": [], - "name": "E" + "fields": [] }, { "$class": "concerto.metamodel.TransactionDeclaration", + "name": "T", "isAbstract": false, "identified": { "$class": "concerto.metamodel.IdentifiedBy", @@ -294,8 +374,7 @@ "isArray": false, "isOptional": false } - ], - "name": "T" + ] } ] } diff --git a/packages/concerto-core/test/data/model/personResolved.json b/packages/concerto-core/test/data/model/personResolved.json index 6bfb937823..40d561c928 100644 --- a/packages/concerto-core/test/data/model/personResolved.json +++ b/packages/concerto-core/test/data/model/personResolved.json @@ -17,6 +17,7 @@ "enumDeclarations": [ { "$class": "concerto.metamodel.EnumDeclaration", + "name": "Gender", "fields": [ { "$class": "concerto.metamodel.EnumFieldDeclaration", @@ -30,19 +31,19 @@ "$class": "concerto.metamodel.EnumFieldDeclaration", "name": "OTHER" } - ], - "name": "Gender" + ] } ], "classDeclarations": [ { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Individual", "isAbstract": true, - "fields": [], - "name": "Individual" + "fields": [] }, { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Person", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -52,6 +53,10 @@ "fields": [ { "$class": "concerto.metamodel.StringFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.StringRegexValidator", + "regex": "/[a-zA-Z]*/u" + }, "name": "firstName", "isArray": false, "isOptional": false @@ -135,11 +140,11 @@ "isArray": false, "isOptional": true } - ], - "name": "Person" + ] }, { "$class": "concerto.metamodel.ParticipantDeclaration", + "name": "Employee", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -159,11 +164,11 @@ "isArray": false, "isOptional": false } - ], - "name": "Employee" + ] }, { "$class": "concerto.metamodel.ConceptDeclaration", + "name": "Address", "isAbstract": false, "identified": { "$class": "concerto.metamodel.Identified" @@ -224,11 +229,11 @@ "isArray": false, "isOptional": false } - ], - "name": "Address" + ] }, { "$class": "concerto.metamodel.ConceptDeclaration", + "name": "USAddress", "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -238,39 +243,114 @@ "fields": [ { "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "lower": -365, + "upper": 365 + }, "name": "zip4", "isArray": false, "isOptional": false }, + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "upper": 365 + }, + "name": "zip41", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.IntegerFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.IntegerDomainValidator", + "lower": -365 + }, + "name": "zip42", + "isArray": false, + "isOptional": false + }, { "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "lower": -365, + "upper": 365 + }, "name": "zip5", "isArray": false, "isOptional": false }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "upper": 365 + }, + "name": "zip51", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.LongFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.LongDomainValidator", + "lower": -365 + }, + "name": "zip52", + "isArray": false, + "isOptional": false + }, { "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "lower": -3.14, + "upper": 3.14 + }, "name": "zip6", "isArray": false, "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "lower": -3.14 + }, + "name": "zip61", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel.DoubleFieldDeclaration", + "validator": { + "$class": "concerto.metamodel.DoubleDomainValidator", + "upper": 3.14 + }, + "name": "zip62", + "isArray": false, + "isOptional": false } - ], - "name": "USAddress" + ] }, { "$class": "concerto.metamodel.AssetDeclaration", + "name": "A", "isAbstract": false, - "fields": [], - "name": "A" + "fields": [] }, { "$class": "concerto.metamodel.EventDeclaration", + "name": "E", "isAbstract": false, - "fields": [], - "name": "E" + "fields": [] }, { "$class": "concerto.metamodel.TransactionDeclaration", + "name": "T", "isAbstract": false, "identified": { "$class": "concerto.metamodel.IdentifiedBy", @@ -305,8 +385,7 @@ "isArray": false, "isOptional": false } - ], - "name": "T" + ] } ] } From 609c93fed036ef20905e0f1a1ddc4ee9844778a4 Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 4 Aug 2021 11:27:30 -0400 Subject: [PATCH 16/17] feature(metamodel) Add support for decorators Signed-off-by: jeromesimeon --- packages/concerto-core/api.txt | 6 + packages/concerto-core/changelog.txt | 2 +- .../concerto-core/lib/introspect/metamodel.js | 123 +++++++++++++++++- .../concerto-core/test/data/model/person.cto | 6 + .../concerto-core/test/data/model/person.json | 84 +++++++++++- .../test/data/model/personResolved.json | 84 +++++++++++- 6 files changed, 300 insertions(+), 5 deletions(-) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 9790c85142..91574bd2c9 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -115,6 +115,9 @@ class ModelFileDownloader { + string resolveName() + object resolveTypeNames() + object enumFieldToMetaModel() + + object decoratorArgToMetaModel() + + object decoratorToMetaModel() + + object decoratorsToMetaModel() + object fieldToMetaModel() + object relationshipToMetaModel() + object enumDeclToMetaModel() @@ -122,6 +125,9 @@ class ModelFileDownloader { + object declToMetaModel() + object modelToMetaModel() + object modelFileToMetaModel() + + string decoratorArgFromMetaModel() + + string decoratorFromMetaModel() + + string decoratorsFromMetaModel() + string fieldFromMetaModel() + string declFromMetaModel() + string ctoFromMetaModel() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 31da00f99b..281fdc7cb3 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 {1af0901bd065706dc1409d1e3482fa48} 2021-07-13 +Version 1.0.5 {16fb2d5684ec917532a19428c74f1ebf} 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 ba4744c321..fed6b91422 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -339,7 +339,6 @@ function resolveTypeNames(metaModel, table) { * @return {object} the metamodel for this field */ function enumFieldToMetaModel(ast) { - // console.log(`FIELD ${JSON.stringify(ast)}`); const field = {}; field.$class = 'concerto.metamodel.EnumFieldDeclaration'; @@ -350,13 +349,68 @@ function enumFieldToMetaModel(ast) { return field; } +/** + * Create metamodel for a decorator argument + * @param {object} ast - the AST for the decorator argument + * @return {object} the metamodel for this decorator argument + */ +function decoratorArgToMetaModel(ast) { + const decoratorArg = {}; + switch (ast.type) { + case 'String': + decoratorArg.$class = 'concerto.metamodel.DecoratorString'; + decoratorArg.value = ast.value; + break; + case 'Number': + decoratorArg.$class = 'concerto.metamodel.DecoratorNumber'; + decoratorArg.value = ast.value; + break; + case 'Boolean': + decoratorArg.$class = 'concerto.metamodel.DecoratorBoolean'; + decoratorArg.value = ast.value; + break; + default: + break; + } + + return decoratorArg; +} + +/** + * Create metamodel for a decorator + * @param {object} ast - the AST for the decorator + * @return {object} the metamodel for this decorator + */ +function decoratorToMetaModel(ast) { + const decorator = { + $class: 'concerto.metamodel.Decorator', + name: ast.name, + }; + if (ast.arguments && ast.arguments.list) { + if (!ast.arguments.list[0]) { + decorator.arguments = []; + } else { + decorator.arguments = ast.arguments.list.map(decoratorArgToMetaModel); + } + } + return decorator; +} + +/** + * Create metamodel for a list of decorators + * @param {object} ast - the AST for the decorators + * @return {object} the metamodel for the decorators + */ +function decoratorsToMetaModel(ast) { + return ast.map(decoratorToMetaModel); +} + /** * Create metamodel for a class field * @param {object} ast - the AST for the field * @return {object} the metamodel for this field */ function fieldToMetaModel(ast) { - // console.log(`FIELD ${JSON.stringify(ast)}`); const field = {}; // Field name @@ -375,6 +429,12 @@ function fieldToMetaModel(ast) { } // XXX Can it be missing? const type = ast.propertyType.name; + + // Handle decorators + if (ast.decorators && ast.decorators.length > 0) { + field.decorators = decoratorsToMetaModel(ast.decorators); + } + switch (type) { case 'Integer': field.$class = 'concerto.metamodel.IntegerFieldDeclaration'; @@ -586,6 +646,11 @@ function classDeclToMetaModel(ast) { } } + // Handle decorators + if (ast.decorators && ast.decorators.length > 0) { + decl.decorators = decoratorsToMetaModel(ast.decorators); + } + // Class fields decl.fields = []; for (let n = 0; n < ast.body.declarations.length; n++) { @@ -678,6 +743,53 @@ function modelFileToMetaModel(modelFile, validate) { return modelToMetaModel(modelFile.ast, validate); } +/** + * Create decorator argument string from a metamodel + * @param {object} mm - the metamodel + * @return {string} the string for the decorator argument + */ +function decoratorArgFromMetaModel(mm) { + let result = ''; + switch (mm.$class) { + case 'concerto.metamodel.DecoratorString': + result += `"${mm.value}"`; + break; + default: + result += `${mm.value}`; + break; + } + return result; +} + +/** + * Create decorator string from a metamodel + * @param {object} mm - the metamodel + * @return {string} the string for the decorator + */ +function decoratorFromMetaModel(mm) { + let result = ''; + result += `@${mm.name}`; + if (mm.arguments) { + result += '('; + result += mm.arguments.map(decoratorArgFromMetaModel).join(','); + result += ')'; + } + return result; +} + +/** + * Create decorators string from a metamodel + * @param {object} mm - the metamodel + * @param {string} prefix - indentation + * @return {string} the string for the decorators + */ +function decoratorsFromMetaModel(mm, prefix) { + let result = ''; + result += mm.map(decoratorFromMetaModel).join(`\n${prefix}`); + result += `\n${prefix}`; + return result; +} + /** * Create a field string from a metamodel * @param {object} mm - the metamodel @@ -688,6 +800,9 @@ function fieldFromMetaModel(mm) { let defaultString = ''; let validatorString = ''; + if (mm.decorators) { + result += decoratorsFromMetaModel(mm.decorators, ' '); + } if (mm.$class === 'concerto.metamodel.RelationshipDeclaration') { result += '-->'; } else { @@ -783,6 +898,10 @@ function fieldFromMetaModel(mm) { */ function declFromMetaModel(mm) { let result = ''; + if (mm.decorators) { + result += decoratorsFromMetaModel(mm.decorators, ''); + } + if (mm.isAbstract) { result += 'abstract '; } diff --git a/packages/concerto-core/test/data/model/person.cto b/packages/concerto-core/test/data/model/person.cto index 3a5ee5e069..c6d47348c6 100644 --- a/packages/concerto-core/test/data/model/person.cto +++ b/packages/concerto-core/test/data/model/person.cto @@ -56,7 +56,13 @@ concept Address identified { o Boolean isPrivate default=false } +@Address("x",1,"y","foo","z",true) +@Address2() +@Address3 concept USAddress extends Address { + @Zip("x",1,"y","foo","z",true) + @Zip2() + @Zip3 o Integer zip4 range=[-365,365] o Integer zip41 range=[,365] o Integer zip42 range=[-365,] diff --git a/packages/concerto-core/test/data/model/person.json b/packages/concerto-core/test/data/model/person.json index 7b4082270c..6d5412d672 100644 --- a/packages/concerto-core/test/data/model/person.json +++ b/packages/concerto-core/test/data/model/person.json @@ -226,6 +226,47 @@ { "$class": "concerto.metamodel.ConceptDeclaration", "name": "USAddress", + "decorators": [ + { + "$class": "concerto.metamodel.Decorator", + "name": "Address", + "arguments": [ + { + "$class": "concerto.metamodel.DecoratorString", + "value": "x" + }, + { + "$class": "concerto.metamodel.DecoratorNumber", + "value": 1 + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "y" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "foo" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "z" + }, + { + "$class": "concerto.metamodel.DecoratorBoolean", + "value": true + } + ] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Address2", + "arguments": [] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Address3" + } + ], "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -241,7 +282,48 @@ }, "name": "zip4", "isArray": false, - "isOptional": false + "isOptional": false, + "decorators": [ + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip", + "arguments": [ + { + "$class": "concerto.metamodel.DecoratorString", + "value": "x" + }, + { + "$class": "concerto.metamodel.DecoratorNumber", + "value": 1 + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "y" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "foo" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "z" + }, + { + "$class": "concerto.metamodel.DecoratorBoolean", + "value": true + } + ] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip2", + "arguments": [] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip3" + } + ] }, { "$class": "concerto.metamodel.IntegerFieldDeclaration", diff --git a/packages/concerto-core/test/data/model/personResolved.json b/packages/concerto-core/test/data/model/personResolved.json index 40d561c928..ca459a99d2 100644 --- a/packages/concerto-core/test/data/model/personResolved.json +++ b/packages/concerto-core/test/data/model/personResolved.json @@ -234,6 +234,47 @@ { "$class": "concerto.metamodel.ConceptDeclaration", "name": "USAddress", + "decorators": [ + { + "$class": "concerto.metamodel.Decorator", + "name": "Address", + "arguments": [ + { + "$class": "concerto.metamodel.DecoratorString", + "value": "x" + }, + { + "$class": "concerto.metamodel.DecoratorNumber", + "value": 1 + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "y" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "foo" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "z" + }, + { + "$class": "concerto.metamodel.DecoratorBoolean", + "value": true + } + ] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Address2", + "arguments": [] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Address3" + } + ], "isAbstract": false, "superType": { "$class": "concerto.metamodel.TypeIdentifier", @@ -250,7 +291,48 @@ }, "name": "zip4", "isArray": false, - "isOptional": false + "isOptional": false, + "decorators": [ + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip", + "arguments": [ + { + "$class": "concerto.metamodel.DecoratorString", + "value": "x" + }, + { + "$class": "concerto.metamodel.DecoratorNumber", + "value": 1 + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "y" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "foo" + }, + { + "$class": "concerto.metamodel.DecoratorString", + "value": "z" + }, + { + "$class": "concerto.metamodel.DecoratorBoolean", + "value": true + } + ] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip2", + "arguments": [] + }, + { + "$class": "concerto.metamodel.Decorator", + "name": "Zip3" + } + ] }, { "$class": "concerto.metamodel.IntegerFieldDeclaration", From a5ced229ede45c53b2ac5530eb637c2fcf1ecd7f Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Wed, 4 Aug 2021 13:59:06 -0400 Subject: [PATCH 17/17] fix(metamodel) Remove unused DecoratorIdentifier Signed-off-by: jeromesimeon --- packages/concerto-core/lib/introspect/metamodel.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index fed6b91422..bdc57183fe 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -60,11 +60,6 @@ concept TypeIdentifier { o String fullyQualifiedName optional } -concept DecoratorIdentifier extends DecoratorLiteral { - o TypeIdentifier identifier - o Boolean isArray default=false -} - concept Decorator { o String name o DecoratorLiteral[] arguments optional