diff --git a/lib/metamodel.cto b/lib/metamodel.cto index 9963280..f707810 100644 --- a/lib/metamodel.cto +++ b/lib/metamodel.cto @@ -208,6 +208,10 @@ concept LongDomainValidator { o Long upper optional } +concept AliasedType{ + o String name + o String aliasedName +} abstract concept Import { o String namespace o String uri optional @@ -222,6 +226,7 @@ concept ImportType extends Import { concept ImportTypes extends Import { o String[] types + o AliasedType[] aliasedTypes optional } concept Model { diff --git a/lib/metamodel.json b/lib/metamodel.json index 6ee7941..379fb57 100644 --- a/lib/metamodel.json +++ b/lib/metamodel.json @@ -997,6 +997,25 @@ } ] }, + { + "$class":"concerto.metamodel@1.0.0.ConceptDeclaration", + "name":"AliasedType", + "isAbstract":false, + "properties":[ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "aliasedName", + "isArray": false, + "isOptional": false + } + ] + }, { "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", "name": "Import", @@ -1053,6 +1072,16 @@ "name": "types", "isArray": true, "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "aliasedTypes", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "AliasedType" + }, + "isArray": true, + "isOptional": true } ], "superType": { @@ -1060,6 +1089,7 @@ "name": "Import" } }, + { "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", "name": "Model", diff --git a/lib/metamodelutil.js b/lib/metamodelutil.js index 7512ed2..c39e724 100644 --- a/lib/metamodelutil.js +++ b/lib/metamodelutil.js @@ -76,12 +76,21 @@ function createNameTable(priorModels, metaModel) { } table[imp.name] = namespace; } else if (imp.$class === `${MetaModelNamespace}.ImportTypes`) { - for (const type of imp.types) { + // Create a map of aliased types if they exist, otherwise initialize an empty map. + const aliasedMap = imp.aliasedTypes + ? new Map(imp.aliasedTypes.map(({ name, aliasedName }) => [name, aliasedName])) + : new Map(); + imp.types.forEach((type) => { + // 'localName' is the identifier used to refer to the imported type, as it can be aliased.. + const localName = aliasedMap.get(type) || type; + + // Verify if the type declaration exists in the model file. + // Here, 'type' refers to the actual declaration name within the model file that is being imported. if (!findDeclaration(modelFile, type)) { throw new Error(`Declaration ${type} in namespace ${namespace} not found`); } - table[type] = namespace; - } + table[localName] = namespace; + }); } else { (modelFile.declarations || []).forEach((decl) => { table[decl.name] = namespace; diff --git a/test/cto/aliasedImport.json b/test/cto/aliasedImport.json new file mode 100644 index 0000000..aa28b3f --- /dev/null +++ b/test/cto/aliasedImport.json @@ -0,0 +1,104 @@ +{ + "$class": "concerto.metamodel@1.0.0.Models", + "models": [ + { + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "org.saluja", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "doc", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false, + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 35, + "line": 4, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 49, + "line": 5, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ], + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 22, + "line": 3, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 50, + "line": 5, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "org.test", + "imports": [ + { + "$class": "concerto.metamodel@1.0.0.ImportTypes", + "namespace": "org.saluja", + "types": [ + "doc" + ], + "aliasedTypes": [ + { + "$class": "concerto.metamodel@1.0.0.AliasedType", + "name": "doc", + "aliasedName": "d" + } + ] + } + ], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "file", + "isAbstract": false, + "properties": [], + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 50, + "line": 5, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 75, + "line": 6, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + } + }, + "superType": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "d" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/cto/aliasedImportResolved.json b/test/cto/aliasedImportResolved.json new file mode 100644 index 0000000..f2e11cb --- /dev/null +++ b/test/cto/aliasedImportResolved.json @@ -0,0 +1,105 @@ +{ + "$class": "concerto.metamodel@1.0.0.Models", + "models": [ + { + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "org.saluja", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "doc", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false, + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 35, + "line": 4, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 49, + "line": 5, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ], + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 22, + "line": 3, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 50, + "line": 5, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "org.test", + "imports": [ + { + "$class": "concerto.metamodel@1.0.0.ImportTypes", + "namespace": "org.saluja", + "types": [ + "doc" + ], + "aliasedTypes": [ + { + "$class": "concerto.metamodel@1.0.0.AliasedType", + "name": "doc", + "aliasedName": "d" + } + ] + } + ], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "file", + "isAbstract": false, + "properties": [], + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 50, + "line": 5, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 75, + "line": 6, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + } + }, + "superType": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "d", + "namespace": "org.saluja" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/metamodelutil.js b/test/metamodelutil.js index 98e7b07..10e4ebb 100644 --- a/test/metamodelutil.js +++ b/test/metamodelutil.js @@ -100,6 +100,74 @@ describe('MetaModel (Car)', () => { }); }); +describe('MetaModel aliasing', () => { + + it('should convert a CTO model to its metamodel with name resolution', async () => { + const ModelPath = path.resolve(__dirname, './cto/aliasedImport.json'); + const Model = JSON.parse(fs.readFileSync(ModelPath, 'utf8')); + const MetaModelResolved = JSON.parse(fs.readFileSync(path.resolve(__dirname, './cto/aliasedImportResolved.json'), 'utf8')); + + const mm1r = MetaModelUtil.resolveLocalNamesForAll(Model); + mm1r.should.deep.equal(MetaModelResolved); + }); + + it('Should throw if name not found',async()=>{ + const model = { + '$class': 'concerto.metamodel@1.0.0.Models', + 'models': [ + { + '$class': 'concerto.metamodel@1.0.0.Model', + decorators: [], + namespace: 'org.vehicle', + imports: [], + declarations: [] + }, + { + '$class': 'concerto.metamodel@1.0.0.Model', + decorators: [], + namespace: 'org.test', + imports: [ + { + '$class': 'concerto.metamodel@1.0.0.ImportTypes', + namespace: 'org.vehicle', + types: [ + 'wheel' + ], + aliasedTypes: [ + { + '$class': 'concerto.metamodel@1.0.0.AliasedType', + name: 'wheel', + aliasedName: 'w' + } + ] + } + ], + declarations: [ + { + '$class': 'concerto.metamodel@1.0.0.ConceptDeclaration', + name: 'car', + isAbstract: false, + properties: [ + { + '$class': 'concerto.metamodel@1.0.0.ObjectProperty', + name: 'wheels', + type: { + '$class': 'concerto.metamodel@1.0.0.TypeIdentifier', + name: 'w' + }, + isArray: true, + isOptional: false, + } + ], + } + ] + } + ] + + }; + (()=>MetaModelUtil.resolveLocalNamesForAll(model)).should.throw(); + }); +}); describe('MetaModel (with Maps & Scalars)', () => { process.env.ENABLE_MAP_TYPE = 'true'; // TODO Remove on release of MapType const modelPath = path.resolve(__dirname, './cto/mapsImported.json'); @@ -186,6 +254,16 @@ describe('importFullyQualifiedNames', () => { const result = MetaModelUtil.importFullyQualifiedNames(ast); result.should.deep.equal(['test.Foo', 'test.Bar']); }); + it('should return imports when aliasing', async () => { + const ast = { + $class: 'concerto.metamodel@1.0.0.ImportTypes', + namespace: 'test', + types: ['Foo', 'Bar'], + aliasedTypes:{'f':'Foo','b':'Bar'} + }; + const result = MetaModelUtil.importFullyQualifiedNames(ast); + result.should.deep.equal(['test.Foo', 'test.Bar']); + }); }); it('should throw for unrecognized import', async () => {