Skip to content

Commit f18a848

Browse files
chore(core): decorateModels perf config (#1022)
* chore(core): decorateModels perf config Signed-off-by: sanketshevkar <[email protected]> * chore(core): make argument optional Signed-off-by: sanketshevkar <[email protected]> * chore(core): test coverage Signed-off-by: sanketshevkar <[email protected]> * chore(core): fastMode for model decoration Signed-off-by: sanketshevkar <[email protected]> * chore(core): fastMode for model decoration Signed-off-by: sanketshevkar <[email protected]> * chore(core): fastMode renamed to dangerouslyFast Signed-off-by: sanketshevkar <[email protected]> * chore(core): code refactor Signed-off-by: sanketshevkar <[email protected]> --------- Signed-off-by: sanketshevkar <[email protected]>
1 parent c0d0f69 commit f18a848

File tree

12 files changed

+207
-13
lines changed

12 files changed

+207
-13
lines changed

packages/concerto-core/api.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class BaseModelManager {
3535
+ void addDecoratorFactory(DecoratorFactory)
3636
+ boolean derivesFrom(string,string)
3737
+ object resolveMetaModel(object)
38-
+ void fromAst(ast)
38+
+ void fromAst(ast,object?,object?)
3939
+ void getAst(boolean?,boolean?)
4040
+ BaseModelManager filter(FilterFunction)
4141
}
@@ -60,7 +60,7 @@ class DecoratorManager {
6060
+ ModelManager validate(decoratorCommandSet,ModelFile[]) throws Error
6161
+ object migrateTo(decoratorCommandSet,string)
6262
+ boolean canMigrate(decoratorCommandSet,DCS_VERSION)
63-
+ ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?,boolean?,boolean?)
63+
+ ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?,boolean?,boolean?,boolean?,boolean?,boolean?)
6464
+ ExtractDecoratorsResult extractDecorators(ModelManager,object,boolean,string,boolean)
6565
+ ExtractDecoratorsResult extractVocabularies(ModelManager,object,boolean,string,boolean)
6666
+ ExtractDecoratorsResult extractNonVocabDecorators(ModelManager,object,boolean,string,boolean)

packages/concerto-core/changelog.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
# Note that the latest public API is documented using JSDocs and is available in api.txt.
2525
#
2626

27-
Version 3.20.5 {3be18f4d17eb0e65e9a9ffc369231bf6} 2025-02-03
27+
Version 3.20.5 {a18e77c836e19a4badcbe83d6c7e7c1e} 2025-02-03
2828
- vocabulary support for namespace scoped decorators
2929

3030
Version 3.20.4 {335406876b7005f0a2e6f9ca3e0d0dbf} 2025-01-21

packages/concerto-core/lib/basemodelmanager.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,16 +794,20 @@ class BaseModelManager {
794794
/**
795795
* Populates the model manager from a models metamodel AST
796796
* @param {*} ast the metamodel
797+
* @param {object} [options] - options for the from ast method
798+
* @param {object} [options.disableValidation] - option to disable metamodel validation and just fetch the models, to be used only if the metamodel is already validated
797799
*/
798-
fromAst(ast) {
800+
fromAst(ast, options) {
799801
this.clearModelFiles();
800802
ast.models.forEach( model => {
801803
if(!EXCLUDE_NS.includes(model.namespace)) { // excludes the internal namespaces, already added
802804
const modelFile = new ModelFile( this, model );
803805
this.addModelFile( modelFile, null, null, true );
804806
}
805807
});
806-
this.validateModelFiles();
808+
if (!options?.disableValidation) {
809+
this.validateModelFiles();
810+
}
807811
}
808812

809813
/**

packages/concerto-core/lib/decoratormanager.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const { MetaModelNamespace } = require('@accordproject/concerto-metamodel');
2222
const semver = require('semver');
2323
const DecoratorExtractor = require('./decoratorextractor');
2424
const { Warning, ErrorCodes } = require('@accordproject/concerto-util');
25+
const IllegalModelException = require('./introspect/illegalmodelexception');
2526
const rfdc = require('rfdc')({
2627
circles: true,
2728
proto: false,
@@ -370,10 +371,21 @@ class DecoratorManager {
370371
* @param {boolean} [options.migrate] - migrate the decoratorCommandSet $class to match the dcs model version
371372
* @param {boolean} [options.defaultNamespace] - the default namespace to use for decorator commands that include a decorator without a namespace
372373
* @param {boolean} [options.enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace
374+
* @param {boolean} [options.skipValidationAndResolution] - optional flag to disable both metamodel resolution and validation, only use if you are sure that the model manager has fully resolved models
375+
* @param {boolean} [options.disableMetamodelResolution] - flag to disable metamodel resolution, only use if you are sure that the model manager has fully resolved models
376+
* @param {boolean} [options.disableMetamodelValidation] - flag to disable metamodel validation, only use if you are sure that the models and decorators are already validated
373377
* @returns {ModelManager} a new model manager with the decorations applied
374378
*/
375379
static decorateModels(modelManager, decoratorCommandSet, options) {
376380

381+
if (options?.skipValidationAndResolution) {
382+
if (options?.disableMetamodelResolution === false || !options?.disableMetamodelValidation === false) {
383+
throw new Error('skipValidationAndResolution cannot be used with disableMetamodelResolution or disableMetamodelValidation options as false');
384+
}
385+
options.disableMetamodelResolution = true;
386+
options.disableMetamodelValidation = true;
387+
}
388+
377389
this.migrateAndValidate(modelManager, decoratorCommandSet, options?.migrate, options?.validate, options?.validateCommands);
378390

379391
// we create synthetic imports for all decorator declarations
@@ -394,7 +406,7 @@ class DecoratorManager {
394406
: []);
395407
}).filter(i => i.namespace);
396408
const { namespaceCommandsMap, declarationCommandsMap, propertyCommandsMap, mapElementCommandsMap, typeCommandsMap } = this.getDecoratorMaps(decoratorCommandSet);
397-
const ast = modelManager.getAst(true, true);
409+
const ast = options?.disableMetamodelResolution ? modelManager.getAst(false, true) : modelManager.getAst(true, true);
398410
const decoratedAst = rfdc(ast);
399411
decoratedAst.models.forEach((model) => {
400412
// remove the imports for types defined in this namespace
@@ -453,7 +465,7 @@ class DecoratorManager {
453465
enableMapType,
454466
importAliasing: modelManager.isAliasedTypeEnabled(),
455467
decoratorValidation: modelManager.getDecoratorValidation()});
456-
newModelManager.fromAst(decoratedAst);
468+
newModelManager.fromAst(decoratedAst, { disableValidation: options?.disableMetamodelValidation });
457469
return newModelManager;
458470
}
459471
/**
@@ -683,11 +695,33 @@ class DecoratorManager {
683695
decorated.decorators
684696
? decorated.decorators.push(newDecorator)
685697
: (decorated.decorators = [newDecorator]);
698+
this.checkForDuplicateDecorators(decorated);
686699
} else {
687700
throw new Error(`Unknown command type ${type}`);
688701
}
689702
}
690703

704+
/**
705+
* Checks for duplicate decorators added to a decorated model element.
706+
* @param {*} decoratedAst ast of the property or the declaration to apply the decorator to
707+
* @throws {IllegalModelException} if the decoratedAst has duplicate decorators
708+
* @private
709+
*/
710+
static checkForDuplicateDecorators(decoratedAst) {
711+
const uniqueDecoratorNames = new Set();
712+
decoratedAst.decorators.forEach(d => {
713+
const decoratorName = d.name;
714+
if(!uniqueDecoratorNames.has(decoratorName)) {
715+
uniqueDecoratorNames.add(decoratorName);
716+
} else {
717+
throw new IllegalModelException(
718+
`Duplicate decorator ${decoratorName}`,
719+
decoratedAst.location,
720+
);
721+
}
722+
});
723+
}
724+
691725
/**
692726
* Executes a Command against a Model Namespace, adding
693727
* decorators to the Namespace.
@@ -718,7 +752,8 @@ class DecoratorManager {
718752
*/
719753
static executeCommand(namespace, declaration, command, property, options) {
720754
const { target, decorator, type } = command;
721-
const { name } = ModelUtil.parseNamespace( namespace );
755+
// the namespace version is already validated in the decorateModels method
756+
const { name } = ModelUtil.parseNamespace( namespace, { disableVersionParsing: true } );
722757
if (this.falsyOrEqual(target.namespace, [namespace,name]) &&
723758
this.falsyOrEqual(target.declaration, [declaration.name])) {
724759

packages/concerto-core/lib/modelutil.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,29 +114,40 @@ class ModelUtil {
114114
* its name and version parts. The version of the namespace
115115
* (if present) is parsed using semver.parse.
116116
* @param {string} ns the namespace to parse
117+
* @param {object} [options] optional parsing options
118+
* @param {boolean} [options.disableVersionParsing] if false, the version will be parsed
117119
* @returns {ParseNamespaceResult} the result of parsing
118120
*/
119-
static parseNamespace(ns) {
121+
static parseNamespace(ns, options) {
120122
if(!ns) {
121123
throw new Error('Namespace is null or undefined.');
122124
}
123125

124126
const parts = ns.split('@');
127+
let version = parts[1];
125128
if(parts.length > 2) {
126129
throw new Error(`Invalid namespace ${ns}`);
127130
}
128131

129-
if(parts.length === 2) {
132+
if(parts.length === 2 && !options?.disableVersionParsing) {
133+
// Validate the version using semver
130134
if(!semver.valid(parts[1])) {
131135
throw new Error(`Invalid namespace ${ns}`);
132136
}
137+
version = semver.parse(parts[1]);
138+
}
139+
140+
if (options?.disableVersionParsing) {
141+
return {
142+
name: parts[0],
143+
};
133144
}
134145

135146
return {
136147
name: parts[0],
137148
escapedNamespace: ns.replace('@', '_'),
138149
version: parts.length > 1 ? parts[1] : null,
139-
versionParsed: parts.length > 1 ? semver.parse(parts[1]) : null
150+
versionParsed: parts.length > 1 ? version : null
140151
};
141152
}
142153

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"$class" : "[email protected]",
3+
"name" : "validated",
4+
"version": "1.0.0",
5+
"commands" : [
6+
{
7+
"$class" : "[email protected]",
8+
"type" : "UPSERT",
9+
"target" : {
10+
"$class" : "[email protected]",
11+
"type" : "[email protected]"
12+
},
13+
"decorator" : {
14+
"$class" : "[email protected]",
15+
"name" : "Hide",
16+
"arguments" : [
17+
{
18+
"$class" : "[email protected]",
19+
"value" : true
20+
}
21+
]
22+
}
23+
},
24+
{
25+
"$class" : "[email protected]",
26+
"type" : "APPEND",
27+
"target" : {
28+
"$class" : "[email protected]",
29+
"type" : "[email protected]"
30+
},
31+
"decorator" : {
32+
"$class" : "[email protected]",
33+
"name" : "Hide",
34+
"arguments" : [
35+
{
36+
"$class" : "[email protected]",
37+
"value" : false
38+
}
39+
]
40+
}
41+
}
42+
]
43+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"$class":"[email protected]","decorators":[],"namespace":"[email protected]","imports":[],"declarations":[{"$class":"[email protected]","name":"Person","isAbstract":false,"properties":[{"$class":"[email protected]","name":"firstName","isArray":false,"isOptional":false},{"$class":"[email protected]","name":"lastName","isArray":false,"isOptional":false}]}]}

packages/concerto-core/test/decoratormanager.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const Printer= require('../../concerto-cto/lib/printer');
2323

2424
const chai = require('chai');
2525
const { DEPRECATION_WARNING, CONCERTO_DEPRECATION_001 } = require('@accordproject/concerto-util/lib/errorcodes');
26+
const ModelFile = require('../lib/introspect/modelfile');
2627
require('chai').should();
2728
chai.use(require('chai-things'));
2829
chai.use(require('chai-as-promised'));
@@ -524,6 +525,76 @@ describe('DecoratorManager', () => {
524525
DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs));
525526
}).should.throw(/Unknown command type INVALID/);
526527
});
528+
529+
it('should decorate resolved model without resolving the model again', async function() {
530+
// load a model to decorate
531+
const testModelManager = new ModelManager({strict:true});
532+
const modelAst = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/resolvedValidatedModel.json'), 'utf-8');
533+
const modelFile = new ModelFile(testModelManager, JSON.parse(modelAst));
534+
testModelManager.addModelFile(modelFile);
535+
536+
const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8');
537+
const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs),
538+
{validate: true, disableMetamodelResolution: true});
539+
540+
decoratedModelManager.should.not.be.null;
541+
});
542+
543+
it('should decorate validated model without validating the model again', async function() {
544+
// load a model to decorate
545+
const testModelManager = new ModelManager({strict:true});
546+
const modelAst = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/resolvedValidatedModel.json'), 'utf-8');
547+
const modelFile = new ModelFile(testModelManager, JSON.parse(modelAst));
548+
testModelManager.addModelFile(modelFile);
549+
550+
const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8');
551+
const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs),
552+
{validate: true, disableMetamodelValidation: true});
553+
554+
decoratedModelManager.should.not.be.null;
555+
});
556+
557+
it('should decorate validated and resolved model using fast mode', async function() {
558+
// load a model to decorate
559+
const testModelManager = new ModelManager({strict:true});
560+
const modelAst = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/resolvedValidatedModel.json'), 'utf-8');
561+
const modelFile = new ModelFile(testModelManager, JSON.parse(modelAst));
562+
testModelManager.addModelFile(modelFile);
563+
564+
const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8');
565+
const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs),
566+
{validate: true, skipValidationAndResolution: true});
567+
568+
decoratedModelManager.should.not.be.null;
569+
});
570+
571+
it('should throw error if fast mode is enabled and disableModelResoltion and disableModelValidation are set as false', async function() {
572+
// load a model to decorate
573+
const testModelManager = new ModelManager({strict:true});
574+
const modelAst = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/resolvedValidatedModel.json'), 'utf-8');
575+
const modelFile = new ModelFile(testModelManager, JSON.parse(modelAst));
576+
testModelManager.addModelFile(modelFile);
577+
578+
const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8');
579+
(() => {
580+
DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs),
581+
{validate: true, skipValidationAndResolution: true, disableMetamodelResolution: false, disableMetamodelValidation: false});
582+
}).should.throw(/skipValidationAndResolution cannot be used with disableMetamodelResolution or disableMetamodelValidation options as false/);
583+
});
584+
585+
it('should check for duplicate while appending a decorator from DCS', async function() {
586+
// load a model to decorate
587+
const testModelManager = new ModelManager({strict:true});
588+
const modelAst = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/resolvedValidatedModel.json'), 'utf-8');
589+
const modelFile = new ModelFile(testModelManager, JSON.parse(modelAst));
590+
testModelManager.addModelFile(modelFile);
591+
592+
const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/dcs-with-two-similar-decorators.json'), 'utf-8');
593+
chai.expect(() => {
594+
DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs),
595+
{validate: true, disableMetamodelValidation: true});
596+
}).to.throw('Duplicate decorator Hide');
597+
});
527598
});
528599

529600
describe('#validateCommand', function() {

packages/concerto-core/test/modelutil.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ describe('ModelUtil', function () {
177177
nsInfo.versionParsed.major.should.equal(1);
178178
});
179179

180+
it('valid, with version validation disabled', function() {
181+
const nsInfo = ModelUtil.parseNamespace('[email protected]', { disableVersionParsing: true });
182+
nsInfo.name.should.equal('org.acme');
183+
nsInfo.should.not.have.property('escapedNamespace');
184+
nsInfo.should.not.have.property('version');
185+
nsInfo.should.not.have.property('versionParsed');
186+
});
187+
180188
it('invalid', function() {
181189
(() => {
182190
ModelUtil.parseNamespace(null);

packages/concerto-core/types/lib/basemodelmanager.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,12 @@ declare class BaseModelManager {
348348
/**
349349
* Populates the model manager from a models metamodel AST
350350
* @param {*} ast the metamodel
351+
* @param {object} [options] - options for the from ast method
352+
* @param {object} [options.disableValidation] - option to disable metamodel validation and just fetch the models, to be used only if the metamodel is already validated
351353
*/
352-
fromAst(ast: any): void;
354+
fromAst(ast: any, options?: {
355+
disableValidation?: object;
356+
}): void;
353357
/**
354358
* Get the full ast (metamodel instances) for a modelmanager
355359
* @param {boolean} [resolve] - whether to resolve names

0 commit comments

Comments
 (0)