@accordproject/concerto-core
Version:
Core Implementation for the Concerto Modeling Language
577 lines (565 loc) • 26.3 kB
JavaScript
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
const ModelManager = require('./modelmanager');
const ModelUtil = require('./modelutil');
const { MetaModelNamespace } = require('@accordproject/concerto-metamodel');
/**
* Utility functions to work with
* [DecoratorCommandSet](https://models.accordproject.org/concerto/decorators.cto)
* @memberof module:concerto-core
* @private
*/
class DecoratorExtractor {
/**
* The action to be performed to extract all, only vocab or only non-vocab decorators
*/
static Action = {
EXTRACT_ALL: 0,
EXTRACT_VOCAB: 1,
EXTRACT_NON_VOCAB: 2
};
/**
* Create the DecoratorExtractor.
* @constructor
* @param {boolean} removeDecoratorsFromModel - flag to determine whether to remove decorators from source model
* @param {string} locale - locale for extracted vocabularies
* @param {string} dcs_version - version string
* @param {Object} sourceModelAst - the ast of source models
* @param {int} [action=DecoratorExtractor.Action.EXTRACT_ALL] - the action to be performed
* @param {object} [options] - decorator extractor options
* @param {boolean} [options.enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace
*/
constructor(removeDecoratorsFromModel, locale, dcs_version, sourceModelAst, action = DecoratorExtractor.Action.EXTRACT_ALL, options) {
this.extractionDictionary = {};
this.removeDecoratorsFromModel = removeDecoratorsFromModel;
this.locale = locale;
this.enableDcsNamespaceTarget = options?.enableDcsNamespaceTarget? true : false;
this.dcs_version = dcs_version;
this.sourceModelAst = sourceModelAst;
this.updatedModelAst = sourceModelAst;
this.action = Object.values(DecoratorExtractor.Action).includes(action)? action : DecoratorExtractor.Action.EXTRACT_ALL;
}
/**
* Returns if the decorator is vocab or not
* @param {string} decoractorName - the name of decorator
* @returns {boolean} - returns true if the decorator is a vocabulary decorator else false
* @private
*/
isVocabDecorator(decoractorName) {
return decoractorName === 'Term' || decoractorName.startsWith('Term_');
}
/**
* Adds a key-value pair to a dictionary (object) if the key exists,
* or creates a new key with the provided value.
*
* @param {string} key - The key to add or update.
* @param {any} value - The value to add or update.
* @param {Object} options - options containing target
* @param {string} options.declaration - Target declaration
* @param {string} options.property - Target property
* @param {string} options.mapElement - Target map element
* @private
*/
constructDCSDictionary( key, value, options) {
const val = {
declaration:options?.declaration || '',
property:options?.property || '',
mapElement:options?.mapElement || '',
dcs: JSON.stringify(value),
};
if (this.extractionDictionary[key] && Array.isArray(this.extractionDictionary[key])) {
this.extractionDictionary[key].push(val);
} else {
this.extractionDictionary[key] = [val];
}
}
/**
* Transforms the collected decorators into proper decorator command sets
* @param {Array<Object>} dcsObjects - the collection of collected decorators
* @param {string} namespace - the current namespace
* @param {Array<Object>} decoratorData - the collection of existing decorator command sets
* @returns {Array<Object>} - the collection of decorator command sets
* @private
*/
transformNonVocabularyDecorators(dcsObjects, namespace, decoratorData){
const {name, version} = ModelUtil.parseNamespace(namespace);
const nameOfDcs = name;
const versionOfDcs = version;
if (dcsObjects?.length > 0){
const dcmsForNamespace = {
'$class': `org.accordproject.decoratorcommands@${this.dcs_version}.DecoratorCommandSet`,
'name': nameOfDcs,
'version': versionOfDcs,
'commands': dcsObjects
};
decoratorData.push(dcmsForNamespace);
}
return decoratorData;
}
/**
* Transforms the collected vocabularies into proper vocabulary command sets
* @param {Array<Object>} vocabObject - the collection of collected vocabularies
* @param {string} namespace - the current namespace
* @param {Array<Object>} vocabData - the collection of existing vocabularies command sets
* @returns {Array<Object>} - the collection of vocabularies command sets
* @private
*/
transformVocabularyDecorators(vocabObject, namespace, vocabData){
if (Object.keys(vocabObject).length > 0 ){
let strVoc = '';
strVoc = strVoc + `locale: ${this.locale}\n`;
strVoc = strVoc + `namespace: ${namespace}\n`;
strVoc = strVoc + 'declarations:\n';
Object.keys(vocabObject).forEach(decl =>{
if (vocabObject[decl].term){
strVoc += ` - ${decl}: ${vocabObject[decl].term}\n`;
}
const otherProps = Object.keys(vocabObject[decl]).filter((str)=>str !== 'term' && str !== 'propertyVocabs');
//If a declaration does not have any Term decorator, then add Term_ decorators to yaml
if(otherProps.length > 0){
if (!vocabObject[decl].term){
strVoc += ` - ${decl}: ${decl}\n`;
}
otherProps.forEach(key =>{
strVoc += ` ${key}: ${vocabObject[decl][key]}\n`;
});
}
if (vocabObject[decl].propertyVocabs && Object.keys(vocabObject[decl].propertyVocabs).length > 0){
if (!vocabObject[decl].term && otherProps.length === 0){
strVoc += ` - ${decl}: ${decl}\n`;
}
strVoc += ' properties:\n';
Object.keys(vocabObject[decl].propertyVocabs).forEach(prop =>{
strVoc += ` - ${prop}: ${vocabObject[decl].propertyVocabs[prop].term || ''}\n`;
const otherProps = Object.keys(vocabObject[decl].propertyVocabs[prop]).filter((str)=>str !== 'term');
otherProps.forEach(key =>{
strVoc += ` ${key}: ${vocabObject[decl].propertyVocabs[prop][key]}\n`;
});
});
}
});
vocabData.push(strVoc);
}
return vocabData;
}
/**
* Transforms the collected vocabularies into proper vocabulary command sets
* @param {Array<Object>} vocabObject - the collection of collected vocabularies
* @param {string} namespace - the current namespace
* @param {Array<Object>} vocabData - the collection of existing vocabularies command sets
* @returns {Array<Object>} - the collection of vocabularies command sets
* @private
*/
transformVocabularyDecoratorsV2(vocabObject, namespace, vocabData){
if (Object.keys(vocabObject).length > 0 ){
let strVoc = '';
strVoc = strVoc + `locale: ${this.locale}\n`;
strVoc = strVoc + `namespace: ${namespace}\n`;
if (vocabObject.namespace && Object.keys(vocabObject.namespace).length > 0 ){
if (vocabObject.namespace.term){
strVoc += `term: ${vocabObject.namespace.term}\n`;
}
let otherProps = Object.keys(vocabObject.namespace).filter((str)=>str !== 'term');
otherProps.forEach(key =>{
strVoc += `${key}: ${vocabObject.namespace[key]}\n`;
});
}
if (vocabObject.declarations && Object.keys(vocabObject.declarations).length > 0 ){
strVoc = strVoc + 'declarations:\n';
Object.keys(vocabObject.declarations).forEach(decl =>{
if (vocabObject.declarations[decl].term){
strVoc += ` - ${decl}: ${vocabObject.declarations[decl].term}\n`;
}
const otherProps = Object.keys(vocabObject.declarations[decl]).filter((str)=>str !== 'term' && str !== 'propertyVocabs');
//If a declaration does not have any Term decorator, then add Term_ decorators to yaml
if(otherProps.length > 0){
if (!vocabObject.declarations[decl].term){
strVoc += ` - ${decl}: ${decl}\n`;
}
otherProps.forEach(key =>{
strVoc += ` ${key}: ${vocabObject.declarations[decl][key]}\n`;
});
}
if (vocabObject.declarations[decl].propertyVocabs && Object.keys(vocabObject.declarations[decl].propertyVocabs).length > 0){
if (!vocabObject.declarations[decl].term && otherProps.length === 0){
strVoc += ` - ${decl}: ${decl}\n`;
}
strVoc += ' properties:\n';
Object.keys(vocabObject.declarations[decl].propertyVocabs).forEach(prop =>{
strVoc += ` - ${prop}: ${vocabObject.declarations[decl].propertyVocabs[prop].term || ''}\n`;
const otherProps = Object.keys(vocabObject.declarations[decl].propertyVocabs[prop]).filter((str)=>str !== 'term');
otherProps.forEach(key =>{
strVoc += ` ${key}: ${vocabObject.declarations[decl].propertyVocabs[prop][key]}\n`;
});
});
}
});
}
else{
strVoc = strVoc + 'declarations: []\n';
}
vocabData.push(strVoc);
}
return vocabData;
}
/**
* Constructs Target object for a given model
* @param {string} namespace - the current namespace
* @param {Object} obj - the ast of the model
* @returns {Object} - the target object
* @private
*/
constructTarget(namespace, obj){
const target = {
'$class': `org.accordproject.decoratorcommands@${this.dcs_version}.CommandTarget`,
'namespace':namespace
};
if (obj.declaration && obj.declaration !== ''){
target.declaration = obj.declaration;
}
if (obj.property && obj.property !== ''){
target.property = obj.property;
}
if (obj.mapElement && obj.mapElement !== ''){
target.mapElement = obj.mapElement;
}
return target;
}
/**
* Parses the dict data into an array of decorator jsons
* @param {Array<Object>} dcsObjects - the array of collected dcs objects
* @param {Object} dcs - the current dcs json to be parsed
* @param {String} DCS_VERSION - the version string
* @param {Object} target - target object for the command
* @returns {Array<Object>} - the array of collected dcs objects with the current dcs
* @private
*/
parseNonVocabularyDecorators(dcsObjects, dcs, DCS_VERSION, target){
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);
return dcsObjects;
}
/**
* @param {Object} dictVoc - the collection of collected vocabularies
* @param {Object} decl - the declaration object
* @param {Object} dcs - the current dcs json to be parsed
* @returns {Object} - the collection of collected vocabularies with current dcs
* @private
*/
parseVocabularies(dictVoc, decl, dcs){
dictVoc[decl.declaration] = dictVoc[decl.declaration] || { propertyVocabs: {} };
if (decl.property !== ''){
if (!dictVoc[decl.declaration].propertyVocabs[decl.property]){
dictVoc[decl.declaration].propertyVocabs[decl.property] = {};
}
if (dcs.name === 'Term'){
dictVoc[decl.declaration].propertyVocabs[decl.property].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
dictVoc[decl.declaration].propertyVocabs[decl.property][extensionKey] = dcs.arguments[0].value;
}
}
else if (decl.mapElement !== ''){
if (!dictVoc[decl.declaration].propertyVocabs[decl.mapElement]){
dictVoc[decl.declaration].propertyVocabs[decl.mapElement] = {};
}
if (dcs.name === 'Term'){
dictVoc[decl.declaration].propertyVocabs[decl.mapElement].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
dictVoc[decl.declaration].propertyVocabs[decl.mapElement][extensionKey] = dcs.arguments[0].value;
}
}
else {
if (dcs.name === 'Term'){
dictVoc[decl.declaration].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
dictVoc[decl.declaration][extensionKey] = dcs.arguments[0].value;
}
}
return dictVoc;
}
/**
* @param {Object} vocabObject - the collection of collected vocabularies
* @param {Object} vocabTarget - the declaration object
* @param {Object} dcs - the current dcs json to be parsed
* @returns {Object} - the collection of collected vocabularies with current dcs
* @private
*/
parseVocabulariesV2(vocabObject, vocabTarget, dcs){
//If the vocabTarget declaration is empty, then it is a namespace level vocabulary
if(vocabTarget.declaration === ''){
vocabObject.namespace = vocabObject.namespace || {};
if (dcs.name === 'Term'){
vocabObject.namespace.term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
if(extensionKey === 'namespace' || extensionKey === 'locale' || extensionKey === 'declarations'){
throw new Error(`Invalid vocabulary key: ${extensionKey}. The key should not be one of the reserved keys: namespace, locale, declarations`);
}
vocabObject.namespace[extensionKey] = dcs.arguments[0].value;
}
return vocabObject;
}
vocabObject.declarations = vocabObject.declarations || {};
vocabObject.declarations[vocabTarget.declaration] = vocabObject.declarations[vocabTarget.declaration] || { propertyVocabs: {} };
if (vocabTarget.property !== ''){
if (!vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.property]){
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.property] = {};
}
if (dcs.name === 'Term'){
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.property].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
if(extensionKey === vocabTarget.property){
throw new Error(`Invalid vocabulary key: "${extensionKey}". The key should not be the name of the current property.`);
}
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.property][extensionKey] = dcs.arguments[0].value;
}
}
else if (vocabTarget.mapElement !== ''){
if (!vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.mapElement]){
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.mapElement] = {};
}
if (dcs.name === 'Term'){
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.mapElement].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
if(extensionKey === vocabTarget.mapElement){
throw new Error(`Invalid vocabulary key: "${extensionKey}". The key should not be the name of the current property.`);
}
vocabObject.declarations[vocabTarget.declaration].propertyVocabs[vocabTarget.mapElement][extensionKey] = dcs.arguments[0].value;
}
}
else {
if (dcs.name === 'Term'){
vocabObject.declarations[vocabTarget.declaration].term = dcs.arguments[0].value;
}
else {
const extensionKey = dcs.name.split('Term_')[1];
if(extensionKey === 'properties' || extensionKey === vocabTarget.declaration){
throw new Error(`Invalid vocabulary key: "${extensionKey}". The key cannot be a reserved word such as "properties" or the name of the current declaration.`);
}
vocabObject.declarations[vocabTarget.declaration][extensionKey] = dcs.arguments[0].value;
}
}
return vocabObject;
}
/**
* parses the extracted decorators and generates arrays of decorator command set and vocabularies
*
* @returns {Object} - constructed DCS Dict and processed models ast
* @private
*/
transformDecoratorsAndVocabularies(){
let decoratorData = [];
let vocabData = [];
Object.keys(this.extractionDictionary).forEach(namespace => {
const jsonData = this.extractionDictionary[namespace];
let dcsObjects = [];
let vocabObject = {};
jsonData.forEach(obj =>{
const decos = JSON.parse(obj.dcs);
const target = this.constructTarget(namespace, obj);
decos.forEach(dcs =>{
const isVocab = this.isVocabDecorator(dcs.name);
if (!isVocab && this.action !== DecoratorExtractor.Action.EXTRACT_VOCAB){
dcsObjects = this.parseNonVocabularyDecorators(dcsObjects, dcs, this.dcs_version, target);
}
if (isVocab && this.action !== DecoratorExtractor.Action.EXTRACT_NON_VOCAB){
vocabObject = this.enableDcsNamespaceTarget ? this.parseVocabulariesV2(vocabObject, obj, dcs) : this.parseVocabularies(vocabObject, obj, dcs);
}
});
});
if(this.action !== DecoratorExtractor.Action.EXTRACT_VOCAB){
decoratorData = this.transformNonVocabularyDecorators(dcsObjects, namespace, decoratorData);
}
if(this.action !== DecoratorExtractor.Action.EXTRACT_NON_VOCAB){
vocabData = this.enableDcsNamespaceTarget ? this.transformVocabularyDecoratorsV2(vocabObject, namespace, vocabData) : this.transformVocabularyDecorators(vocabObject, namespace, vocabData);
}
});
return {
decoratorCommandSet: decoratorData,
vocabularies: vocabData
};
}
/**
* Filter vocab or non-vocab decorators
* @param {Object} decorators - the collection of decorators
* @returns {Object} - the collection of filtered decorators
* @private
*/
filterOutDecorators(decorators){
if(!this.removeDecoratorsFromModel){
return decorators;
}
if (this.action === DecoratorExtractor.Action.EXTRACT_ALL){
return undefined;
}
else if(this.action === DecoratorExtractor.Action.EXTRACT_VOCAB){
return decorators.filter((dcs) => !this.isVocabDecorator(dcs.name));
}
else{
return decorators.filter((dcs) => this.isVocabDecorator(dcs.name));
}
}
/**
* Process the map declarations to extract the decorators.
*
* @param {Object} declaration - The source AST of the model
* @param {string} namespace - namespace of the model
* @returns {Object} - processed map declarations ast
* @private
*/
processMapDeclaration(declaration, namespace){
if (declaration.key){
if (declaration.key.decorators){
const constructOptions = {
declaration: declaration.name,
mapElement: 'KEY'
};
this.constructDCSDictionary(namespace, declaration.key.decorators, constructOptions);
declaration.key.decorators = this.filterOutDecorators(declaration.key.decorators);
}
}
if (declaration.value){
if (declaration.value.decorators){
const constructOptions = {
declaration: declaration.name,
mapElement: 'VALUE'
};
this.constructDCSDictionary(namespace, declaration.value.decorators, constructOptions);
declaration.value.decorators = this.filterOutDecorators(declaration.value.decorators);
}
}
return declaration;
}
/**
* Process the properties to extract the decorators.
*
* @param {Object} sourceProperties - The source AST of the property
* @param {string} declarationName - The name of source declaration
* @param {string} namespace - namespace of the model
* @returns {Object} - processed properties ast
* @private
*/
processProperties(sourceProperties, declarationName, namespace){
const processedProperties = sourceProperties.map(property => {
if (property.decorators){
const constructOptions = {
declaration: declarationName,
property: property.name
};
this.constructDCSDictionary(namespace, property.decorators, constructOptions );
property.decorators = this.filterOutDecorators(property.decorators);
}
return property;
});
return processedProperties;
}
/**
* Process the declarations to extract the decorators.
*
* @param {Object} sourceDecl - The source AST of the model
* @param {string} namespace - namespace of the model
* @returns {Object} - processed declarations ast
* @private
*/
processDeclarations(sourceDecl, namespace){
const processedDecl = sourceDecl.map(decl => {
if (decl.decorators) {
const constructOptions = {
declaration: decl.name,
};
this.constructDCSDictionary(namespace, decl.decorators, constructOptions);
decl.decorators = this.filterOutDecorators(decl.decorators);
}
if (decl.$class === `${MetaModelNamespace}.MapDeclaration`) {
const processedMapDecl = this.processMapDeclaration(decl, namespace);
decl = processedMapDecl;
}
if (decl.properties) {
const processedProperties = this.processProperties(decl.properties, decl.name, namespace);
decl.properties = processedProperties;
}
return decl;
});
return processedDecl;
}
/**
* Process the models to extract the decorators.
*
* @private
*/
processModels(){
const processedModels = this.sourceModelAst.models.map(model =>{
if ((model?.decorators?.length > 0)){
this.constructDCSDictionary(model.namespace, model.decorators, {});
model.decorators = this.filterOutDecorators(model.decorators);
}
const processedDecl = this.processDeclarations(model.declarations, model.namespace);
model.declarations = processedDecl;
return model;
});
this.updatedModelAst = {
...this.updatedModelAst,
models: processedModels
};
}
/**
* Collects the decorators and vocabularies and updates the modelManager depending
* on the options.
*
* @returns {Object} - constructed DCS Dict and processed models ast
* @private
*/
extract() {
this.processModels();
const updatedModelManager = new ModelManager();
updatedModelManager.fromAst(this.updatedModelAst);
const extractedDecosAndVocabs = this.transformDecoratorsAndVocabularies();
return {
updatedModelManager,
decoratorCommandSet: extractedDecosAndVocabs.decoratorCommandSet,
vocabularies: extractedDecosAndVocabs.vocabularies
};
}
}
module.exports = DecoratorExtractor;