diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index c999ea922..23be2927e 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -124,6 +124,7 @@ class Declaration extends Decorated { + boolean isEnum() + boolean isClassDeclaration() + boolean isScalarDeclaration() + + boolean isMapDeclaration() } class Decorator { + void constructor(ClassDeclaration|Property,Object) throws IllegalModelException @@ -180,6 +181,7 @@ class MapKeyType extends Decorated { + String toString() + boolean isKey() + boolean isValue() + + string getNamespace() } class MapValueType extends Decorated { + void constructor(MapDeclaration,Object) throws IllegalModelException @@ -190,6 +192,7 @@ class MapValueType extends Decorated { + String toString() + boolean isKey() + boolean isValue() + + string getNamespace() } + ModelManager newMetaModelManager() + object validateMetaModel() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 6af538a7d..66500462d 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 3.13.2 {dccc690753912cf87e7ceec56d949058} 2023-10-18 +- Add getNamespace method to key type and value type of maps + Version 3.13.1 {f5a9a1ea6a64865843a3abb77798cbb0} 2023-10-18 - Add migrate option to DecoratorManager options diff --git a/packages/concerto-core/lib/decoratormanager.js b/packages/concerto-core/lib/decoratormanager.js index 46ee7b443..63a045934 100644 --- a/packages/concerto-core/lib/decoratormanager.js +++ b/packages/concerto-core/lib/decoratormanager.js @@ -54,14 +54,14 @@ enum CommandType { /** * Which models elements to add the decorator to. Any null - * elements are 'wildcards'. + * elements are 'wildcards'. */ concept CommandTarget { o String namespace optional o String declaration optional o String property optional o String[] properties optional // property and properties are mutually exclusive - o String type optional + o String type optional o MapElement mapElement optional } @@ -438,6 +438,8 @@ class DecoratorManager { if (this.falsyOrEqual(target.type, declaration.value.$class)) { this.applyDecorator(declaration.value, type, decorator); } + } else { + this.applyDecorator(declaration, type, decorator); } } else if (!(target.property || target.properties || target.type)) { this.applyDecorator(declaration, type, decorator); diff --git a/packages/concerto-core/lib/introspect/declaration.js b/packages/concerto-core/lib/introspect/declaration.js index 0bd8b59e5..033c2afe4 100644 --- a/packages/concerto-core/lib/introspect/declaration.js +++ b/packages/concerto-core/lib/introspect/declaration.js @@ -177,6 +177,15 @@ class Declaration extends Decorated { isScalarDeclaration() { return false; } + + /** + * Returns true if this class is the definition of a map-declaration. + * + * @return {boolean} true if the class is a map-declaration + */ + isMapDeclaration() { + return false; + } } module.exports = Declaration; diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 51a3b31e6..5ffa2a3c0 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -163,6 +163,14 @@ class MapKeyType extends Decorated { isValue() { return false; } + + /** + * Return the namespace of this map key. + * @return {string} namespace - a namespace. + */ + getNamespace() { + return this.modelFile.getNamespace(); + } } module.exports = MapKeyType; diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 0ea4d8a6a..b7e392274 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -185,6 +185,14 @@ class MapValueType extends Decorated { isValue() { return true; } + + /** + * Return the namespace of this map value. + * @return {string} namespace - a namespace. + */ + getNamespace() { + return this.modelFile.getNamespace(); + } } module.exports = MapValueType; diff --git a/packages/concerto-core/test/data/decoratorcommands/map-declaration.json b/packages/concerto-core/test/data/decoratorcommands/map-declaration.json index 92db66dd9..9c4dfa25f 100644 --- a/packages/concerto-core/test/data/decoratorcommands/map-declaration.json +++ b/packages/concerto-core/test/data/decoratorcommands/map-declaration.json @@ -3,6 +3,20 @@ "name" : "web", "version": "1.0.0", "commands" : [ + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "MapDeclarationDecorator", + "arguments" : [] + } + }, { "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", "type" : "APPEND", diff --git a/packages/concerto-core/test/decoratormanager.js b/packages/concerto-core/test/decoratormanager.js index 4003aee13..6ef3fa6a7 100644 --- a/packages/concerto-core/test/decoratormanager.js +++ b/packages/concerto-core/test/decoratormanager.js @@ -188,6 +188,21 @@ describe('DecoratorManager', () => { decoratorCity2Property.should.not.be.null; }); + it('should decorate the specified MapDeclaration', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + dictionary.should.not.be.null; + dictionary.getDecorator('MapDeclarationDecorator').should.not.be.null; + }); + it('should decorate the specified element on the specified Map Declaration (Map Key)', async function() { // load a model to decorate const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); @@ -284,7 +299,7 @@ describe('DecoratorManager', () => { dictionary.value.getDecorator('DecoratesValueByType').should.not.be.null; }); - it('should decorate both Key and Value elements on the specified Map Declaration', async function() { + it('should decorate Declaration, Key and Value elements on the specified Map Declaration', async function() { // load a model to decorate const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); @@ -297,6 +312,7 @@ describe('DecoratorManager', () => { const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); dictionary.should.not.be.null; + dictionary.getDecorator('MapDeclarationDecorator').should.not.be.null; dictionary.key.getDecorator('Baz').should.not.be.null; dictionary.value.getDecorator('Baz').should.not.be.null; }); diff --git a/packages/concerto-core/test/introspect/declaration.js b/packages/concerto-core/test/introspect/declaration.js index 90c5f1f25..df712bac0 100644 --- a/packages/concerto-core/test/introspect/declaration.js +++ b/packages/concerto-core/test/introspect/declaration.js @@ -47,6 +47,12 @@ describe('Declaration', () => { }); }); + describe('#isMapDeclaration', () => { + it('should be false', () => { + declaration.isMapDeclaration().should.equal(false); + }); + }); + describe('#isSystemIdentified', () => { it('should be false', () => { declaration.isSystemIdentified().should.equal(false); diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index bc3d73ac4..49ef1af6b 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -757,4 +757,12 @@ describe('MapDeclaration', () => { declaration.getValue().getParent().should.equal(declaration); }); }); + + describe('#getNamespace', () => { + it('should return the correct namespace for a Map Declaration Key and Value', () => { + let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.string.cto', MapDeclaration); + declaration.getKey().getNamespace().should.equal('com.acme@1.0.0'); + declaration.getValue().getNamespace().should.equal('com.acme@1.0.0'); + }); + }); }); diff --git a/packages/concerto-vocabulary/lib/vocabulary.js b/packages/concerto-vocabulary/lib/vocabulary.js index 7ac709b25..6a446b48c 100644 --- a/packages/concerto-vocabulary/lib/vocabulary.js +++ b/packages/concerto-vocabulary/lib/vocabulary.js @@ -144,16 +144,47 @@ class Vocabulary { * @returns {*} an object with missingTerms and additionalTerms properties */ validate(modelFile) { - const getOwnProperties = (d) => { - // ensures we have a valid return, even for scalars - return d.getOwnProperties?.() ? d.getOwnProperties?.() : []; + const getOwnProperties = (declaration) => { + // ensures we have a valid return, even for scalars and map-declarations + if(declaration.isMapDeclaration()) { + return [declaration.getKey(), declaration.getValue()]; + } else { + return declaration.getOwnProperties?.() ? declaration.getOwnProperties?.() : []; + } }; + + const getPropertyName = (property) => { + if(property.isKey?.()) { + return 'KEY'; + } else if(property.isValue?.()) { + return 'VALUE'; + } else { + return property.getName(); + } + }; + + const checkPropertyExists = (k, p) => { + const declaration = modelFile.getLocalType(Object.keys(k)[0]); + const property = Object.keys(p)[0]; + if(declaration.isMapDeclaration()) { + if (property === 'KEY') { + return true; + } else if(property === 'VALUE') { + return true; + } else { + return false; + } + } else { + return declaration.getOwnProperty(Object.keys(p)[0]); + } + }; + const result = { missingTerms: modelFile.getAllDeclarations().flatMap( d => this.getTerm(d.getName()) - ? getOwnProperties(d).flatMap( p => this.getTerm(d.getName(), p.getName()) ? null : `${d.getName()}.${p.getName()}`) + ? getOwnProperties(d).flatMap( p => this.getTerm(d.getName(), getPropertyName(p)) ? null : `${d.getName()}.${getPropertyName(p)}`) : d.getName() ).filter( i => i !== null), additionalTerms: this.content.declarations.flatMap( k => modelFile.getLocalType(Object.keys(k)[0]) - ? Array.isArray(k.properties) ? k.properties.flatMap( p => modelFile.getLocalType(Object.keys(k)[0]).getOwnProperty(Object.keys(p)[0]) ? null : `${Object.keys(k)[0]}.${Object.keys(p)[0]}`) : null + ? Array.isArray(k.properties) ? k.properties.flatMap( p => checkPropertyExists(k, p) ? null : `${Object.keys(k)[0]}.${Object.keys(p)[0]}`) : null : k ).filter( i => i !== null) }; diff --git a/packages/concerto-vocabulary/lib/vocabularymanager.js b/packages/concerto-vocabulary/lib/vocabularymanager.js index de15b1813..7bc650e09 100644 --- a/packages/concerto-vocabulary/lib/vocabularymanager.js +++ b/packages/concerto-vocabulary/lib/vocabularymanager.js @@ -205,7 +205,18 @@ class VocabularyManager { resolveTerms(modelManager, namespace, locale, declarationName, propertyName) { const modelFile = modelManager.getModelFile(namespace); const classDecl = modelFile ? modelFile.getType(declarationName) : null; - const property = propertyName ? classDecl ? classDecl.getProperty(propertyName) : null : null; + let property; + if(classDecl && !classDecl.isScalarDeclaration()) { + if(classDecl.isMapDeclaration()) { + if(propertyName === 'KEY') { + property = classDecl.getKey(); + } else if(propertyName === 'VALUE') { + property = classDecl.getValue(); + } + } else { + property = propertyName ? classDecl ? classDecl.getProperty(propertyName) : null : null; + } + } return this.getTerms(property ? property.getNamespace() : namespace, locale, property ? property.getParent().getName() : declarationName, propertyName); } @@ -286,6 +297,16 @@ class VocabularyManager { 'commands': [] }; + const getPropertyNames = (declaration) => { + if (declaration.getProperties) { + return declaration.getProperties().map(property => property.getName()); + } else if(declaration.isMapDeclaration?.()) { + return ['KEY', 'VALUE']; + } else { + return []; + } + }; + modelManager.getModelFiles().forEach(model => { model.getAllDeclarations().forEach(decl => { const terms = this.resolveTerms(modelManager, model.getNamespace(), locale, decl.getName()); @@ -336,11 +357,13 @@ class VocabularyManager { }); } - decl.getProperties?.().forEach(property => { - const propertyTerms = this.resolveTerms(modelManager, model.getNamespace(), locale, decl.getName(), property.getName()); + const propertyNames = getPropertyNames(decl); + propertyNames.forEach(propertyName => { + const propertyTerms = this.resolveTerms(modelManager, model.getNamespace(), locale, decl.getName(), propertyName); if (propertyTerms) { Object.keys(propertyTerms).forEach( term => { - if(term === property.getName()) { + const propertyType = propertyName === 'KEY' || propertyName === 'VALUE' ? 'mapElement' : 'property'; + if(term === propertyName) { decoratorCommandSet.commands.push({ '$class': `${DC_NAMESPACE}.Command`, 'type': 'UPSERT', @@ -348,7 +371,7 @@ class VocabularyManager { '$class': `${DC_NAMESPACE}.CommandTarget`, 'namespace': model.getNamespace(), 'declaration': decl.getName(), - 'property': property.getName() + [propertyType]: propertyName }, 'decorator': { '$class': `${MetaModelNamespace}.Decorator`, @@ -370,7 +393,7 @@ class VocabularyManager { '$class': `${DC_NAMESPACE}.CommandTarget`, 'namespace': model.getNamespace(), 'declaration': decl.getName(), - 'property': property.getName() + [propertyType]: propertyName }, 'decorator': { '$class': `${MetaModelNamespace}.Decorator`, diff --git a/packages/concerto-vocabulary/test/__snapshots__/vocabularymanager.js.snap b/packages/concerto-vocabulary/test/__snapshots__/vocabularymanager.js.snap index 21f341566..672c4137e 100644 --- a/packages/concerto-vocabulary/test/__snapshots__/vocabularymanager.js.snap +++ b/packages/concerto-vocabulary/test/__snapshots__/vocabularymanager.js.snap @@ -220,6 +220,103 @@ Object { }, "type": "UPSERT", }, + Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.Command", + "decorator": Object { + "$class": "concerto.metamodel@1.0.0.Decorator", + "arguments": Array [ + Object { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "Address of the vehicle", + }, + ], + "name": "Term", + }, + "target": Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "declaration": "Address", + "namespace": "org.acme@1.0.0", + }, + "type": "UPSERT", + }, + Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.Command", + "decorator": Object { + "$class": "concerto.metamodel@1.0.0.Decorator", + "arguments": Array [ + Object { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "Registered address of the vehicle owner", + }, + ], + "name": "Term_description", + }, + "target": Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "declaration": "Address", + "namespace": "org.acme@1.0.0", + }, + "type": "UPSERT", + }, + Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.Command", + "decorator": Object { + "$class": "concerto.metamodel@1.0.0.Decorator", + "arguments": Array [ + Object { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "View Address of vehicle owner", + }, + ], + "name": "Term_tooltip", + }, + "target": Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "declaration": "Address", + "namespace": "org.acme@1.0.0", + }, + "type": "UPSERT", + }, + Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.Command", + "decorator": Object { + "$class": "concerto.metamodel@1.0.0.Decorator", + "arguments": Array [ + Object { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "vin of the vehicle", + }, + ], + "name": "Term", + }, + "target": Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "declaration": "Address", + "mapElement": "KEY", + "namespace": "org.acme@1.0.0", + }, + "type": "UPSERT", + }, + Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.Command", + "decorator": Object { + "$class": "concerto.metamodel@1.0.0.Decorator", + "arguments": Array [ + Object { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "address of the vehicle", + }, + ], + "name": "Term", + }, + "target": Object { + "$class": "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "declaration": "Address", + "mapElement": "VALUE", + "namespace": "org.acme@1.0.0", + }, + "type": "UPSERT", + }, Object { "$class": "org.accordproject.decoratorcommands@0.3.0.Command", "decorator": Object { diff --git a/packages/concerto-vocabulary/test/org.acme@1.0.0.cto b/packages/concerto-vocabulary/test/org.acme@1.0.0.cto index 40958bdae..504a7e1b1 100644 --- a/packages/concerto-vocabulary/test/org.acme@1.0.0.cto +++ b/packages/concerto-vocabulary/test/org.acme@1.0.0.cto @@ -14,6 +14,11 @@ asset Vehicle identified by vin { o Color color } +map Address { + o String + o String +} + asset Truck extends Vehicle { o Double weight -} \ No newline at end of file +} diff --git a/packages/concerto-vocabulary/test/org.acme@1.0.0_en-gb.voc b/packages/concerto-vocabulary/test/org.acme@1.0.0_en-gb.voc index 40ab77711..b380a0ff0 100644 --- a/packages/concerto-vocabulary/test/org.acme@1.0.0_en-gb.voc +++ b/packages/concerto-vocabulary/test/org.acme@1.0.0_en-gb.voc @@ -4,4 +4,9 @@ declarations: - Truck: A lorry description: A heavy goods vehicle - Color: A colour - - Milkfloat \ No newline at end of file + - Milkfloat + - Address: Address of the vehicle + description: Registered address of the vehicle owner + tooltip: View Address of vehicle owner + properties: + - BAD_KEY: mispelled KEY spelling diff --git a/packages/concerto-vocabulary/test/org.acme@1.0.0_en.voc b/packages/concerto-vocabulary/test/org.acme@1.0.0_en.voc index 2b890ec3a..d051e1722 100644 --- a/packages/concerto-vocabulary/test/org.acme@1.0.0_en.voc +++ b/packages/concerto-vocabulary/test/org.acme@1.0.0_en.voc @@ -17,4 +17,8 @@ declarations: tooltip: Truck weight - horsePower: The horse power of the truck description: The horse power of the truck - tooltip: Truck HP \ No newline at end of file + tooltip: Truck HP + - Address: Registered address of the vehicle + properties: + - KEY: vin of the vehicle + - VALUE: address of the vehicle diff --git a/packages/concerto-vocabulary/test/org.acme@1.0.0_fr.voc b/packages/concerto-vocabulary/test/org.acme@1.0.0_fr.voc index 09123f6e4..d0079fcbc 100644 --- a/packages/concerto-vocabulary/test/org.acme@1.0.0_fr.voc +++ b/packages/concerto-vocabulary/test/org.acme@1.0.0_fr.voc @@ -3,4 +3,7 @@ namespace: org.acme@1.0.0 declarations: - Vehicle: Véhicule properties: - - vin: Le numéro d'identification du véhicule (NIV) \ No newline at end of file + - vin: Le numéro d'identification du véhicule (NIV) + - Address: Adresse + properties: + - KEY: NIV du véhicule diff --git a/packages/concerto-vocabulary/test/vocabularymanager.js b/packages/concerto-vocabulary/test/vocabularymanager.js index fcc30385b..520cd22ca 100644 --- a/packages/concerto-vocabulary/test/vocabularymanager.js +++ b/packages/concerto-vocabulary/test/vocabularymanager.js @@ -34,6 +34,7 @@ let vocabularyManager = null; describe('VocabularyManager', () => { beforeEach(() => { + process.env.ENABLE_MAP_TYPE = 'true'; // TODO Remove on release of MapType modelManager = new ModelManager(); const model = fs.readFileSync('./test/org.acme@1.0.0.cto', 'utf-8'); modelManager.addCTOModel(model); @@ -127,14 +128,14 @@ describe('VocabularyManager', () => { const voc = vocabularyManager.getVocabulary('org.acme@1.0.0', 'en'); voc.should.not.be.null; const terms = voc.getTerms(); - terms.length.should.equal(4); + terms.length.should.equal(5); }); it('getTerms - fr', () => { const voc = vocabularyManager.getVocabulary('org.acme@1.0.0', 'fr'); voc.should.not.be.null; const terms = voc.getTerms(); - terms.length.should.equal(1); + terms.length.should.equal(2); }); it('getTerms - lookup declaration', () => { @@ -367,10 +368,10 @@ describe('VocabularyManager', () => { result.additionalVocabularies[0].getNamespace().should.equal('com.example@1.0.0'); result.vocabularies['org.acme@1.0.0/en'].additionalTerms.should.have.members(['Vehicle.model', 'Truck.horsePower']); result.vocabularies['org.acme@1.0.0/en'].missingTerms.should.have.members(['Color.RED', 'Color.BLUE', 'Color.GREEN', 'SSN', 'Vehicle.color']); - result.vocabularies['org.acme@1.0.0/en-gb'].additionalTerms.should.have.members(['Milkfloat']); - result.vocabularies['org.acme@1.0.0/fr'].missingTerms.should.have.members(['Color', 'SSN', 'VIN', 'Vehicle.color', 'Truck']); + result.vocabularies['org.acme@1.0.0/en-gb'].additionalTerms.should.have.members(['Milkfloat', 'Address.BAD_KEY']); + result.vocabularies['org.acme@1.0.0/fr'].missingTerms.should.have.members(['Color', 'SSN', 'VIN', 'Vehicle.color', 'Address.VALUE', 'Truck']); result.vocabularies['org.acme@1.0.0/fr'].additionalTerms.should.have.members([]); - result.vocabularies['org.acme@1.0.0/zh-cn'].missingTerms.should.have.members(['SSN', 'VIN', 'Truck']); + result.vocabularies['org.acme@1.0.0/zh-cn'].missingTerms.should.have.members(['SSN', 'VIN', 'Address', 'Truck']); result.vocabularies['org.acme@1.0.0/zh-cn'].additionalTerms.should.have.members([]); });