Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): decorator multi property #713

Merged
merged 10 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16,988 changes: 12,612 additions & 4,376 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"devDependencies": {
"colors": "1.4.0",
"coveralls": "3.1.0",
"dayjs": "1.10.8",
"dayjs": "1.11.10",
"eslint": "8.2.0",
"jsdoc": "^4.0.2",
"glob": "^7.2.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/concerto-core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ class Concerto {
class DecoratorManager {
+ ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?)
+ void validateCommand(ModelManager,command)
+ Boolean falsyOrEqual(string|,string|)
+ Boolean falsyOrEqual(string||,string[])
+ void applyDecorator(decorated,string,newDecorator)
+ void executeCommand(string,declaration,command)
+ void executePropertyCommand(property,command)
}
+ string[] intersect()
+ boolean isUnversionedNamespaceEqual()
class Factory {
+ string newId()
Expand Down
2 changes: 1 addition & 1 deletion packages/concerto-core/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#

Version 3.12.5 {ced48c7ab94de207be87584468517f3e} 2023-09-08
Version 3.13.0 {a7060663ad5bb322ec4ee760baa7ab1a} 2023-10-01
- Update DecoratorManager to support multiple value compare

Version 3.12.4 {7738d5490ea8438677e1d21d704bb5aa} 2023-08-31
Expand Down
203 changes: 148 additions & 55 deletions packages/concerto-core/lib/decoratormanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Factory = require('./factory');
const ModelUtil = require('./modelutil');

const DCS_MODEL = `concerto version "^3.0.0"
namespace org.accordproject.decoratorcommands@0.2.0
namespace org.accordproject.decoratorcommands@0.3.0

import [email protected]

Expand Down Expand Up @@ -48,6 +48,7 @@ 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
}

Expand All @@ -66,11 +67,24 @@ concept Command {
concept DecoratorCommandSet {
o String name
o String version
o DecoratorCommandSetReference[] includes optional
o DecoratorCommandSetReference[] includes optional // not yet supported
o Command[] commands
}
`;

/**
* Intersection of two string arrays
* @param {string[]} a the first array
* @param {string[]} b the second array
* @returns {string[]} returns the intersection of a and b (i.e. an
* array of the elements they have in common)
*/
function intersect(a, b) {
const setA = new Set(a);
const setB = new Set(b);
const intersection = new Set([...setA].filter((x) => setB.has(x)));
return Array.from(intersection);
}

/**
* Returns true if the unversioned namespace for a model
Expand All @@ -81,7 +95,7 @@ concept DecoratorCommandSet {
* model file equals unversionedNamespace
*/
function isUnversionedNamespaceEqual(modelFile, unversionedNamespace) {
const { name } = ModelUtil.parseNamespace( modelFile.getNamespace() );
const { name } = ModelUtil.parseNamespace(modelFile.getNamespace());
return name === unversionedNamespace;
}

Expand All @@ -104,24 +118,34 @@ class DecoratorManager {
* @returns {ModelManager} a new model manager with the decorations applied
*/
static decorateModels(modelManager, decoratorCommandSet, options) {
if(options?.validate) {
const validationModelManager = new ModelManager({strict:true, metamodelValidation: true, addMetamodel: true});
if (options?.validate) {
const validationModelManager = new ModelManager({
strict: true,
metamodelValidation: true,
addMetamodel: true,
});
validationModelManager.addModelFiles(modelManager.getModelFiles());
validationModelManager.addCTOModel(DCS_MODEL, '[email protected]');
validationModelManager.addCTOModel(
DCS_MODEL,
'[email protected]'
);
const factory = new Factory(validationModelManager);
const serializer = new Serializer(factory, validationModelManager);
serializer.fromJSON(decoratorCommandSet);
if(options?.validateCommands) {
decoratorCommandSet.commands.forEach(command => {
DecoratorManager.validateCommand(validationModelManager, command);
if (options?.validateCommands) {
decoratorCommandSet.commands.forEach((command) => {
DecoratorManager.validateCommand(
validationModelManager,
command
);
});
}
}
const ast = modelManager.getAst(true);
const decoratedAst = JSON.parse(JSON.stringify(ast));
decoratedAst.models.forEach(model => {
model.declarations.forEach(decl => {
decoratorCommandSet.commands.forEach(command => {
decoratedAst.models.forEach((model) => {
model.declarations.forEach((decl) => {
decoratorCommandSet.commands.forEach((command) => {
this.executeCommand(model.namespace, decl, command);
});
});
Expand All @@ -137,48 +161,96 @@ class DecoratorManager {
* @param {*} command the decorator command
*/
static validateCommand(validationModelManager, command) {
if(command.target.type) {
validationModelManager.resolveType( 'DecoratorCommand.type', command.target.type);
if (command.target.type) {
validationModelManager.resolveType(
'DecoratorCommand.type',
command.target.type
);
}
let modelFile = null;
if(command.target.namespace) {
modelFile = validationModelManager.getModelFile(command.target.namespace);
if(!modelFile) {
const { name, version } = ModelUtil.parseNamespace(command.target.namespace);
if(!version) {
if (command.target.namespace) {
modelFile = validationModelManager.getModelFile(
command.target.namespace
);
if (!modelFile) {
const { name, version } = ModelUtil.parseNamespace(
command.target.namespace
);
if (!version) {
// does the model file exist with any version?
modelFile = validationModelManager.getModelFiles()
modelFile = validationModelManager
.getModelFiles()
.find((m) => isUnversionedNamespaceEqual(m, name));
}
}
}
if(command.target.namespace && !modelFile) {
throw new Error(`Decorator Command references namespace "${command.target.namespace}" which does not exist: ${JSON.stringify(command, null, 2)}`);
if (command.target.namespace && !modelFile) {
throw new Error(
`Decorator Command references namespace "${
command.target.namespace
}" which does not exist: ${JSON.stringify(command, null, 2)}`
);
}

if(command.target.namespace && command.target.declaration) {
validationModelManager.resolveType( 'DecoratorCommand.target.declaration', `${modelFile.getNamespace()}.${command.target.declaration}`);
if (command.target.namespace && command.target.declaration) {
validationModelManager.resolveType(
'DecoratorCommand.target.declaration',
`${modelFile.getNamespace()}.${command.target.declaration}`
);
}
if (command.target.properties && command.target.property) {
throw new Error(
'Decorator Command references both property and properties. You must either reference a single property or a list of properites.'
);
}
if(command.target.namespace && command.target.declaration && command.target.property) {
const decl = validationModelManager.getType(`${modelFile.getNamespace()}.${command.target.declaration}`);
if (
command.target.namespace &&
command.target.declaration &&
command.target.property
) {
const decl = validationModelManager.getType(
`${modelFile.getNamespace()}.${command.target.declaration}`
);
const property = decl.getProperty(command.target.property);
if(!property) {
throw new Error(`Decorator Command references property "${command.target.namespace}.${command.target.declaration}.${command.target.property}" which does not exist.`);
if (!property) {
throw new Error(
`Decorator Command references property "${command.target.namespace}.${command.target.declaration}.${command.target.property}" which does not exist.`
);
}
}
if (
command.target.namespace &&
command.target.declaration &&
command.target.properties
) {
const decl = validationModelManager.getType(
`${modelFile.getNamespace()}.${command.target.declaration}`
);
command.target.properties.forEach((commandProperty) => {
const property = decl.getProperty(commandProperty);
if (!property) {
throw new Error(
`Decorator Command references property "${command.target.namespace}.${command.target.declaration}.${commandProperty}" which does not exist.`
);
}
});
}
}

/**
* Compares two values. If the first argument is falsy
* Compares two arrays. If the first argument is falsy
* the function returns true.
* @param {string | null} test the value to test (lhs)
* @param {string|string[]} values the values to compare (rhs)
* @returns {Boolean} true if the lhs is falsy or values.includes(test)
* @param {string | string[] | null} test the value to test
* @param {string[]} values the values to compare
* @returns {Boolean} true if the test is falsy or the intersection of
* the test and values arrays is not empty (i.e. they have values in common)
*/
static falsyOrEqual(test, values) {
return test
? Array.isArray(values) ? values.includes(test) : test === values
: true;
return Array.isArray(test)
? intersect(test, values).length > 0
: test
? values.includes(test)
: true;
}

/**
Expand All @@ -190,7 +262,7 @@ class DecoratorManager {
static applyDecorator(decorated, type, newDecorator) {
if (type === 'UPSERT') {
let updated = false;
if(decorated.decorators) {
if (decorated.decorators) {
for (let n = 0; n < decorated.decorators.length; n++) {
let decorator = decorated.decorators[n];
if (decorator.name === newDecorator.name) {
Expand All @@ -201,15 +273,15 @@ class DecoratorManager {
}

if (!updated) {
decorated.decorators ? decorated.decorators.push(newDecorator)
: decorated.decorators = [newDecorator];
decorated.decorators
? decorated.decorators.push(newDecorator)
: (decorated.decorators = [newDecorator]);
}
}
else if (type === 'APPEND') {
decorated.decorators ? decorated.decorators.push(newDecorator)
: decorated.decorators = [newDecorator];
}
else {
} else if (type === 'APPEND') {
decorated.decorators
? decorated.decorators.push(newDecorator)
: (decorated.decorators = [newDecorator]);
} else {
throw new Error(`Unknown command type ${type}`);
}
}
Expand All @@ -224,25 +296,46 @@ class DecoratorManager {
*/
static executeCommand(namespace, declaration, command) {
const { target, decorator, type } = command;
const { name } = ModelUtil.parseNamespace( namespace );
if (this.falsyOrEqual(target.namespace, [namespace,name]) &&
this.falsyOrEqual(target.declaration, [declaration.name])) {
const { name } = ModelUtil.parseNamespace(namespace);
if (
this.falsyOrEqual(target.namespace, [namespace, name]) &&
this.falsyOrEqual(target.declaration, [declaration.name])
) {
if (!target.property && !target.type) {
this.applyDecorator(declaration, type, decorator);
}
else {
} else {
// scalars are declarations but do not have properties
if(declaration.properties) {
declaration.properties.forEach(property => {
if (this.falsyOrEqual(target.property, [property.name]) &&
this.falsyOrEqual(target.type, [property.$class])) {
this.applyDecorator(property, type, decorator);
}
if (declaration.properties) {
declaration.properties.forEach((property) => {
DecoratorManager.executePropertyCommand(
property,
command
);
});
}
}
}
}

/**
* Executes a Command against a Property, adding
* decorators to the Property as required.
* @param {*} property the property
* @param {*} command the Command object from the
* org.accordproject.decoratorcommands model
*/
static executePropertyCommand(property, command) {
const { target, decorator, type } = command;
if (
this.falsyOrEqual(
target.property ? target.property : target.properties,
[property.name]
) &&
this.falsyOrEqual(target.type, [property.$class])
) {
this.applyDecorator(property, type, decorator);
}
}
}

module.exports = DecoratorManager;
Loading