diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index a1d8d7ee9..59242c4a9 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -58,7 +58,6 @@ class DecoratorManager { + ModelManager validate(decoratorCommandSet,ModelFile[]) throws Error + ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?) + Object extractDecorators(ModelManager,object,boolean,string) - ~ Object visitScalarDeclaration(ScalarDeclaration,Object) + void validateCommand(ModelManager,command) + Boolean falsyOrEqual(string||,string[]) + void applyDecorator(decorated,string,newDecorator) diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 88d6a42ef..443f2bd33 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 3.14.2 {7119a636878d9d9c318c89dcfbc913be} 2023-11-07 +Version 3.14.2 {86711a7ba556ad7f2be251f03e99b448} 2023-11-07 - Added DCS and vocabulary extraction support for decoratorManager Version 3.13.1 {f5a9a1ea6a64865843a3abb77798cbb0} 2023-10-18 diff --git a/packages/concerto-core/lib/decoratormanager.js b/packages/concerto-core/lib/decoratormanager.js index e25209aea..b384a3d7e 100644 --- a/packages/concerto-core/lib/decoratormanager.js +++ b/packages/concerto-core/lib/decoratormanager.js @@ -267,14 +267,66 @@ class DecoratorManager { locale:'en', ...options }; - const extractionDictionary={}; - const data = modelManager.accept(this, {options,extractionDictionary,extractDecorators:true}); - const decoratorCommandSet=(this.parseNonVocabularyDecorators(data.extractionDictionary)); - const vocabularies = this.parseVocabularies(data.extractionDictionary,options.locale); + const ast = modelManager.getAst(true); + const decoratedAst = JSON.parse(JSON.stringify(ast)); + let extractionDictionary={}; + const processedModels = decoratedAst.models.map((model)=>{ + if (model.decorators && model.decorators.length>0){ + extractionDictionary=this.constructDCSDictionary(extractionDictionary,model.namespace, model.decorators,'',''); + if (options.removeDecoratorsFromModel){ + model.decorators=null; + } + } + const processedDecl = model.declarations.map((decl)=>{ + if (decl.decorators) { + extractionDictionary=this.constructDCSDictionary(extractionDictionary,model.namespace,decl.decorators,decl.name,''); + } + if (options.removeDecoratorsFromModel){ + decl.decorators=null; + } + if (decl.$class.endsWith('.MapDeclaration')) { + if (decl.key){ + if (decl.key.decorators){ + extractionDictionary=this.constructDCSDictionary(extractionDictionary,model.namespace,decl.key.decorators,decl.name,'','KEY'); + decl.key.decorators=null; + } + } + if (decl.value){ + if (decl.value.decorators){ + extractionDictionary=this.constructDCSDictionary(extractionDictionary,model.namespace,decl.value.decorators,decl.name,'','VALUE'); + decl.value.decorators=null; + } + } + } + if (decl.properties) { + const processedProperties = decl.properties.map((property) => { + if (property.decorators){ + extractionDictionary=this.constructDCSDictionary(extractionDictionary,model.namespace, property.decorators,decl.name,property.name); + } + if (options.removeDecoratorsFromModel){ + property.decorators=null; + } + return property; + }); + decl.properties=processedProperties; + } + return decl; + }); + model.declarations=processedDecl; + return model; + }); + const processedAST={ + ...decoratedAst, + models:processedModels, + }; + const newModelManager = new ModelManager(); + newModelManager.fromAst(processedAST); + const decoratorCommandSet=(this.parseNonVocabularyDecorators(extractionDictionary)); + const vocabularies = this.parseVocabularies(extractionDictionary,options.locale); return { + modelManager:newModelManager, decoratorCommandSet, - vocabularies, - modelManager:data.modelManager + vocabularies }; } /** @@ -293,7 +345,7 @@ class DecoratorManager { const jsonData=decoratorDict[namespace]; const patternToDetermineVocab = /^Term_/i; jsonData.forEach((obj)=>{ - const dcs=(obj.dcs); + const decos=JSON.parse(obj.dcs); const target={ '$class': `org.accordproject.decoratorcommands@${DCS_VERSION}.CommandTarget`, 'namespace':namespace @@ -304,28 +356,33 @@ class DecoratorManager { if (obj.property && obj.property!==''){ target.property=obj.property; } - if (dcs.name!=='Term' && !patternToDetermineVocab.test(dcs.name)){ - const decotatorObj={ - '$class': 'concerto.metamodel@1.0.0.Decorator', - 'name': dcs.name, - }; - if (dcs.arguments){ - const args=dcs.arguments.map(arg=>{ - return { - '$class':arg.$class, - 'value':arg.value - }; - }); - decotatorObj.arguments=args; - } - const dcsObject = { - '$class': `org.accordproject.decoratorcommands@${DCS_VERSION}.Command`, - 'type': 'UPSERT', - 'target': target, - 'decorator': decotatorObj, - }; - dcsObjects.push(dcsObject); + if (obj.mapElement && obj.mapElement!==''){ + target.mapElement=obj.mapElement; } + decos.forEach((dcs)=>{ + if (dcs.name!=='Term' && !patternToDetermineVocab.test(dcs.name)){ + const decotatorObj={ + '$class': 'concerto.metamodel@1.0.0.Decorator', + 'name': dcs.name, + }; + if (dcs.arguments){ + const args=dcs.arguments.map((arg)=>{ + return { + '$class':arg.$class, + 'value':arg.value + }; + }); + decotatorObj.arguments=args; + } + let dcsObject = { + '$class': `org.accordproject.decoratorcommands@${DCS_VERSION}.Command`, + 'type': 'UPSERT', + 'target': target, + 'decorator': decotatorObj, + }; + dcsObjects.push(dcsObject); + } + }); }); const dcmsForNamespace={ '$class': `org.accordproject.decoratorcommands@${DCS_VERSION}.DecoratorCommandSet`, @@ -353,38 +410,40 @@ class DecoratorManager { strVoc=strVoc+`namespace: ${namespace}\n`; strVoc=strVoc+'declarations:\n'; const jsonData=decoratorDict[namespace]; - let dictVoc={}; + const dictVoc={}; jsonData.forEach((obj)=>{ - // check if obj.decl already in dictVoc if (!dictVoc[obj.declaration]){ dictVoc[obj.declaration]={ propertyVocabs:{} }; } - const dcs=(obj.dcs); - if (dcs.name==='Term' || patternToDetermineVocab.test(dcs.name)){ - if (obj.property!==''){ - if(!dictVoc[obj.declaration].propertyVocabs[obj.property]){ - dictVoc[obj.declaration].propertyVocabs[obj.property]={}; - } - if (dcs.name==='Term'){ - dictVoc[obj.declaration].propertyVocabs[obj.property].term=dcs.arguments[0].value; - } - else{ - const extensionKey = dcs.name.split('Term_')[1]; - dictVoc[obj.declaration].propertyVocabs[obj.property][extensionKey]=dcs.arguments[0].value; - } - } - else{ - if (dcs.name==='Term'){ - dictVoc[obj.declaration].term=dcs.arguments[0].value; + const decos=JSON.parse(obj.dcs); + decos.forEach((dcs)=>{ + if (dcs.name==='Term' || patternToDetermineVocab.test(dcs.name)){ + if (obj.property!==''){ + if(!dictVoc[obj.declaration].propertyVocabs[obj.property]){ + dictVoc[obj.declaration].propertyVocabs[obj.property]={}; + } + if (dcs.name==='Term'){ + dictVoc[obj.declaration].propertyVocabs[obj.property].term=dcs.arguments[0].value; + } + else{ + const extensionKey = dcs.name.split('Term_')[1]; + dictVoc[obj.declaration].propertyVocabs[obj.property][extensionKey]=dcs.arguments[0].value; + } } else{ - const extensionKey = dcs.name.split('Term_')[1]; - dictVoc[obj.declaration][extensionKey]=dcs.arguments[0].value; + if (dcs.name==='Term'){ + dictVoc[obj.declaration].term=dcs.arguments[0].value; + } + else{ + const extensionKey = dcs.name.split('Term_')[1]; + dictVoc[obj.declaration][extensionKey]=dcs.arguments[0].value; + } } } - } + }); + }); Object.keys(dictVoc).forEach((decl)=>{ if (dictVoc[decl].term){ @@ -412,204 +471,31 @@ class DecoratorManager { }); return data; } - /** - * Visitor design pattern - * @param {Object} thing - the object being visited - * @param {Object} parameters - the parameter - * @return {Boolean} the result of visiting or null - * @private - */ - static visit(thing, parameters = {}) { - if (thing.isModelManager?.()) { - return this.visitModelManager(thing, parameters); - } else if (thing.isModelFile?.()) { - return this.visitModelFile(thing, parameters); - } else if (thing.isEnum?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isClassDeclaration?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isMapDeclaration?.()) { - return; - } else if (thing.isScalarDeclaration?.()) { - return this.visitScalarDeclaration(thing, parameters); - }else if (thing.isParticipant?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isTransaction?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isEvent?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isAsset?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isTypeScalar?.()) { - return this.visitDeclaration(thing, parameters); - } else if (thing.isField?.()) { - return this.visitField(thing, parameters); - } else if (thing.isRelationship?.()) { - return this.visitField(thing, parameters); - } else if (thing.isEnumValue?.()) { - return this.visitField(thing, parameters); - } else { - throw new Error('Unrecognised ' + JSON.stringify(thing) ); - } - } - - - /** - * Visitor design pattern - * @param {ModelManager} modelManager - the object being visited - * @param {Object} parameters - the parameter - * @return {Object} the result of visiting or null - * @private - */ - static visitModelManager(modelManager, parameters) { - if (parameters.extractDecorators){ - let updatedModelManager=modelManager; - updatedModelManager.modelFiles=modelManager.getModelFiles().map((modelFile) => { - if (modelFile.decorators && modelFile.decorators.length>0){ - parameters.extractionDictionary=this.constructDCSDictionary(parameters.extractionDictionary,modelFile.namespace, modelFile.decorators,'',''); - if (parameters.options.removeDecoratorsFromModel){ - modelFile.removeDecorators(); - } - } - parameters.namespace=modelFile.namespace; - const data=modelFile.accept(this, parameters); - parameters.extractionDictionary=data.extractionDictionary; - modelFile=data.modelFile; - return modelFile; - }); - return {extractionDictionary:parameters.extractionDictionary,modelManager:updatedModelManager}; - } - else{ - return null; - } - } - - /** - * Visitor design pattern - * @param {ModelFile} modelFile - the object being visited - * @param {Object} parameters - the parameter - * @return {Object} the result of visiting or null - * @private - */ - static visitModelFile(modelFile, parameters) { - if (parameters.extractDecorators){ - let updatedModelFile=modelFile; - updatedModelFile.declarations=modelFile.getAllDeclarations() - .map((decl) => { - if (decl.decorators && decl.decorators.length>0){ - parameters.extractionDictionary=this.constructDCSDictionary(parameters.extractionDictionary,parameters.namespace, decl.decorators,decl.name,''); - if (parameters.options.removeDecoratorsFromModel){ - decl.removeDecorators(); - } - } - parameters.declaration= decl.name; - const data=decl.accept(this, parameters); - parameters.extractionDictionary=data.extractionDictionary; - decl=data.declaration; - return decl; - }); - return {extractionDictionary:parameters.extractionDictionary,modelFile:updatedModelFile}; - } - else{ - return null; - } - } - - /** - * Visitor design pattern - * @param {ClassDeclaration} classDeclaration - the object being visited - * @param {Object} parameters - the parameter - * @param {string} type - the type of the declaration - * @return {Object} the result of visiting or null - * @private - */ - static visitDeclaration(classDeclaration, parameters) { - if (parameters.extractDecorators){ - let updatedDeclaration=classDeclaration; - updatedDeclaration.properties=classDeclaration.getOwnProperties().map((property) => { - if (property.decorators && property.decorators.length>0){ - parameters.extractionDictionary=this.constructDCSDictionary(parameters.extractionDictionary,parameters.namespace, property.decorators,parameters.declaration,property.name); - if (parameters.options.removeDecoratorsFromModel){ - property.removeDecorators(); - } - } - return property; - }); - return {extractionDictionary:parameters.extractionDictionary,declaration:updatedDeclaration}; - } - else{ - return null; - } - } - /** - * Visitor design pattern - * @param {Field} field - the object being visited - * @param {Object} parameters - the parameter - * @return {Object} the result of visiting or null - * @private - */ - static visitField(field, parameters) { - if (parameters.extractDecorators){ - return parameters.extractionDictionary; - } - else{ - return null; - } - } - /** - * Visitor design pattern - * @param {ScalarDeclaration} scalarDeclaration - the object being visited - * @param {Object} parameters - the parameter - * @return {Object} the result of visiting or null - * @protected - */ - static visitScalarDeclaration(scalarDeclaration, parameters) { - if (parameters.extractDecorators){ - let updatedScalarDeclaration=scalarDeclaration; - const scalarDeclarationName = scalarDeclaration.getName(); - const decos = scalarDeclaration.getDecorators(); - if (decos){ - if (decos.decorators && decos.decorators.length>0){ - parameters.extractionDictionary=this.constructDCSDictionary(parameters.extractionDictionary,parameters.namespace, decos,scalarDeclarationName,''); - if (parameters.options.removeDecoratorsFromModel){ - updatedScalarDeclaration.removeDecorators(); - } - } - } - return {extractionDictionary:parameters.extractionDictionary,declaration:updatedScalarDeclaration}; - } - else{ - return null; - } - } - /** * Adds a key-value pair to a dictionary (object) if the key exists, * or creates a new key with the provided value. * * @param {Object} dictionary - The dictionary (object) to which to add the key-value pair. * @param {string} key - The key to add or update. - * @param {any} decorators - The value to add or update. + * @param {any} value - The value to add or update. * @param {string} declaration - The target decl. - * @param {string} property The target property. + * @param {string} property - The target property. + * @param {string} mapElement - The target mapElement. * @returns {Object} - constructed DCS Dict * @private */ - static constructDCSDictionary(dictionary, key, decorators, declaration,property) { - decorators.forEach(deco=>{ - const val = { - declaration, - property, - dcs: {name: deco.name, - arguments:deco.ast.arguments - }, - }; - if (dictionary[key] && Array.isArray( dictionary[key])) { - dictionary[key].push(val); - } else { - dictionary[key] = [val]; - } - }); + static constructDCSDictionary(dictionary, key, value, declaration,property,mapElement='') { + const val = { + declaration, + property, + mapElement, + dcs: JSON.stringify(value), + }; + if (dictionary[key] && Array.isArray( dictionary[key])) { + dictionary[key].push(val); + } else { + dictionary[key] = [val]; + } return dictionary; } @@ -836,4 +722,4 @@ class DecoratorManager { } } -module.exports = DecoratorManager; +module.exports = DecoratorManager; \ No newline at end of file diff --git a/packages/concerto-core/lib/introspect/decorated.js b/packages/concerto-core/lib/introspect/decorated.js index b892fc9af..a91c3fd9c 100644 --- a/packages/concerto-core/lib/introspect/decorated.js +++ b/packages/concerto-core/lib/introspect/decorated.js @@ -144,15 +144,6 @@ class Decorated { getDecorators() { return this.decorators; } - /** - * Returns the decorators for this class. - * - */ - removeDecorators() { - this.ast.decorators=null; - this.process(); - } - /** * Returns the decorator for this class with a given name. diff --git a/packages/concerto-core/test/data/decoratorcommands/extract-test.cto b/packages/concerto-core/test/data/decoratorcommands/extract-test.cto new file mode 100644 index 000000000..a8797eb67 --- /dev/null +++ b/packages/concerto-core/test/data/decoratorcommands/extract-test.cto @@ -0,0 +1,46 @@ +namespace test@1.0.0 +@deco +enum Dummy { + @one + o One +} +@scc +scalar SSN extends String default="000-00-0000" + +@M1 +participant participantName identified by participantKey { + @M3 + o String participantKey +} + +@Dummy("term1",2) +asset assetName identified by assetKey { + o String assetKey +} + +map mapName { + @deco(1) + o String + o String +} + +@Term("Person Class") +@Editable +concept Person { + @Term("HI") + @Custom + @Form("inputType", "text") + @New + o String firstName + @Form("inputType", "text") + @New + o String lastName + @Term("some") + @Term_cus("con") + @Form("inputType", "textArea") + @New + o String bio + @Form("inputType", "text") + @New + o String ssn +} \ No newline at end of file diff --git a/packages/concerto-core/test/decoratormanager.js b/packages/concerto-core/test/decoratormanager.js index ae00bfb70..c82688692 100644 --- a/packages/concerto-core/test/decoratormanager.js +++ b/packages/concerto-core/test/decoratormanager.js @@ -17,6 +17,8 @@ const fs = require('fs'); const DecoratorManager = require('../lib/decoratormanager'); const ModelManager = require('../lib/modelmanager'); +const VocabularyManager= require('../../concerto-vocabulary/lib/vocabularymanager'); +const Printer= require('../../concerto-cto/lib/printer'); const chai = require('chai'); require('chai').should(); @@ -472,16 +474,13 @@ describe('DecoratorManager', () => { }); describe('#extractDecorators', function() { - it('should be able to extract decorators and vocabs from a model', async function() { - const testModelManager = new ModelManager({strict:true}); - const modelText = fs.readFileSync('./test/data/decoratorcommands/test-decorated-model.cto', 'utf-8'); + it('should be able to extract decorators and vocabs from a model withoup options', async function() { + const testModelManager = new ModelManager({strict:true,}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/extract-test.cto', 'utf-8'); testModelManager.addCTOModel(modelText, 'test.cto'); - let resp = DecoratorManager.extractDecorators( testModelManager); - let dcs = resp.decoratorCommandSet; - const validationModelResult = dcs.map((decoractor)=>{ - return DecoratorManager.validate((decoractor)); - }); - validationModelResult.should.not.be.null; + const resp = DecoratorManager.extractDecorators( testModelManager); + const dcs = resp.decoratorCommandSet; + dcs.should.not.be.null; }); it('should be able to extract decorators and vocabs from a model without namespace version', async function() { const testModelManager = new ModelManager(); @@ -491,9 +490,55 @@ describe('DecoratorManager', () => { removeDecoratorsFromModel:true, locale:'en' }; - let resp = DecoratorManager.extractDecorators( testModelManager, options); - let dcs = resp.decoratorCommandSet; - dcs.should.not.be.null; + const resp = DecoratorManager.extractDecorators( testModelManager, options); + const vocabs = resp.vocabularies; + vocabs.should.not.be.null; + }); + it('should ensure that extraction and re-application of decorators and vocabs from a model is an identity operation', async function() { + const testModelManager = new ModelManager(); + const sourceCTO = []; + const updatedCTO = []; + const modelTextWithoutNamespace = fs.readFileSync('./test/data/decoratorcommands/extract-test.cto', 'utf-8'); + testModelManager.addCTOModel(modelTextWithoutNamespace, 'test.cto'); + const options = { + removeDecoratorsFromModel:true, + locale:'en' + }; + const namespaceSource = testModelManager.getNamespaces().filter(namespace=>namespace!=='concerto@1.0.0' && namespace!=='concerto'); + namespaceSource.forEach(name=>{ + let model = testModelManager.getModelFile(name); + let modelAst=model.getAst(); + let data = Printer.toCTO(modelAst); + sourceCTO.push(data); + }); + const resp = DecoratorManager.extractDecorators( testModelManager, options); + const dcs = resp.decoratorCommandSet; + const vocabs= resp.vocabularies; + let newModelManager=resp.modelManager; + const vocManager = new VocabularyManager(); + vocabs.forEach(content => { + vocManager.addVocabulary(content); + }); + const vocabKeySet=[]; + const namespaceUpdated = newModelManager.getNamespaces().filter(namespace=>namespace!=='concerto@1.0.0' && namespace!=='concerto'); + namespaceUpdated.forEach(name=>{ + let vocab = vocManager.getVocabulariesForNamespace(name); + vocab.forEach(voc=>vocabKeySet.push(voc.getLocale())); + }); + vocabKeySet.map(voc=>{ + let commandSet = vocManager.generateDecoratorCommands(newModelManager, voc); + newModelManager = DecoratorManager.decorateModels(newModelManager, commandSet); + }); + dcs.forEach(content => { + newModelManager = DecoratorManager.decorateModels(newModelManager, (content)); + }); + namespaceUpdated.forEach(name=>{ + let model = newModelManager.getModelFile(name); + let modelAst=model.getAst(); + let data = Printer.toCTO(modelAst); + updatedCTO.push(data); + }); + sourceCTO.should.be.deep.equal(updatedCTO); }); });