jsmf-core
Version:
Javascript Modelling Framework
521 lines (478 loc) • 17.7 kB
JavaScript
/**
* @license
* ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved
* JavaScript Modelling Framework (JSMF)
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @author J.S. Sottet
* @author N. Biri
* @author A. Vagner
*/
const _ = require('lodash')
, Type = require('./Type')
, Cardinality = require('./Cardinality').Cardinality
let conformsTo, generateId
(function () {
const Common = require('./Common')
conformsTo = Common.conformsTo
generateId = Common.generateId
}).call()
/**
* Creation of a JSMF Class.
*
* The attributes are given in a key value manner: the key is the name of the
* attribute, the valu its type.
*
* The references are also given in a key / value way. The name of the
* references are the keys, the value can has the following attrivutes:
* - type: The target type of the reference
* - cardinality: the cardinality of the reference
* - opposite: the name of the opposite reference
* - oppositeCardinality: the cardinality of the opposite reference
* - associated: the type of the associated data.
*
* @constructor
* @param {string} name - Name of the class
* @param {Class[]} superClasses - The superclasses of the current class
* @param {Object} attributes - the attributes of the class.
* @param {Object} attributes - the references of the class.
*
* @property {string} __name the name of the class
* @property {Class[]} superClasses - the superclasses of this JSMF class
* @property {Object[]} attributes - the attributes of the class
* @property {Object[]} references - the references of the class
* @returns {Class~ClassInstance}
*/
function Class(name, superClasses, attributes, references, flexible) {
/** The generic class instances Class
* @constructor
* @param {Object} attr The initial values of the instance
*/
function ClassInstance(attr) {
Object.defineProperties(this,
{ __jsmf__: {value: elementMeta(ClassInstance)}
})
createAttributes(this, ClassInstance)
createReferences(this, ClassInstance)
_.forEach(attr, (v,k) => {this[k] = v})
}
Object.defineProperties(ClassInstance.prototype, {
conformsTo: {value: function () { return conformsTo(this) }, enumerable: false},
getAssociated : {value: getAssociated, enumerable: false}
})
superClasses = superClasses || []
superClasses = _.isArray(superClasses) ? superClasses : [superClasses]
Object.assign(ClassInstance, {__name: name, superClasses, attributes: {}, references: {}})
ClassInstance.errorCallback = flexible
? onError.silent
: onError.throw
Object.defineProperty(ClassInstance, '__jsmf__', {value: classMeta()})
populateClassFunction(ClassInstance)
if (attributes !== undefined) { ClassInstance.addAttributes(attributes)}
if (references !== undefined) { ClassInstance.addReferences(references)}
return ClassInstance
}
Class.__name = 'Class'
Class.getInheritanceChain = () => [Class]
Class.newInstance = (name, superClasses, attributes, references) => new Class(name, superClasses, attributes, references)
/** Return true if the given object is a JSMF Class.
*/
const isJSMFClass = o => conformsTo(o) === Class
/**
* Returns the InheritanceChain of this class
* @method
* @memberof Class~ClassInstance
*/
function getInheritanceChain() {
return _(this.superClasses)
.reverse()
.reduce((acc, v) => v.getInheritanceChain().concat(acc), [this])
}
/**
* Returns the own and inherited references of this class
* @method
* @memberof Class~ClassInstance
*/
function getAllReferences() {
return _.reduce(
this.getInheritanceChain(),
(acc, cls) => _.reduce(cls.references, (acc2, v, k) => {acc2[k] = v; return acc2}, acc),
{})
}
/**
* Returns the own and inherited attributes of this class
* @method
* @memberof Class~ClassInstance
*/
function getAllAttributes() {
return _.reduce(
this.getInheritanceChain(),
(acc, cls) => _.reduce(cls.attributes, (acc2, v, k) => {acc2[k] = v; return acc2}, acc),
{})
}
/**
* Returns the associated data of a reference or of all the references of an object
* @method
* @memberof Class~ClassInstance
* @param {string} name - The name of the reference to explore if undefined, all the references are returned
*/
function getAssociated(name) {
const path = ['__jsmf__', 'associated']
if (name !== undefined) {
path.push(name)
}
return _.get(this, path)
}
/**
* Add several references to the Class
* @method
* @memberof Class~ClassInstance
* @param {Object} descriptor - The definition of the attributes,
* the keys are the names of the attribute to create.
* The values contains the description of the attribute.
* See {@link Class~ClassInstance#addAttribute} parameters name
* for the supported property name.
*/
function addReferences(descriptor) {
_.forEach(descriptor, (desc, k) =>
this.addReference(
k,
desc.target || desc.type,
desc.cardinality,
desc.opposite,
desc.oppositeCardinality,
desc.associated,
desc.errorCallback,
desc.oppositeErrorCallback)
)
}
/**
* Add a reference to the Class
* @method
* @memberof Class~ClassInstance
* @param {string} name - The reference name
* @param {Class} target - The target class. Note that {@link Class} and {@link Model}
* can be targeted as well, even if they are not formally
* instances of {@link Class}.
* @param {Cardinality} sourceCardinality - The cardinality of the reference
* @param {string} opposite - The name of the ooposite reference if any,
* it can be an existing or a new reference name.
* @param {Cardinality} oppositeCardinality - The cardinality of the opposite reference,
* not used if opposite is not set.
* @param {Class} associated - The type of the associated data linked to this reference
* @param {Function} errorCallback - Defines what to do when wrong types are assigned
* @param {Function} oppositeErrorCallback - Defines what to do when wrong types are
* assigned to the opposite reference
*/
function addReference(name, target, sourceCardinality, opposite, oppositeCardinality, associated, errorCallback, oppositeErrorCallback) {
this.references[name] = { type: target || Type.JSMFAny
, cardinality: Cardinality.check(sourceCardinality)
}
if (opposite !== undefined) {
this.references[name].opposite = opposite
target.references[opposite] =
{ type: this
, cardinality: oppositeCardinality === undefined && target.references[opposite] !== undefined ?
target.references[opposite].cardinality :
Cardinality.check(oppositeCardinality)
, opposite: name
, errorCallback: oppositeErrorCallback === undefined && target.references[opposite] !== undefined
? target.references[opposite].oppositeErrorCallback
: this.errorCallback
}
}
if (associated !== undefined) {
this.references[name].associated = associated
if (opposite !== undefined) {
target.references[opposite].associated = associated
}
}
this.references[name].errorCallback = errorCallback || this.errorCallback
}
/**
* Remove a reference from a class
* @method
* @memberof Class~ClassInstance
* @param {string} name - The name of the reference to remove
* @param {boolean} opposite - true if the opposite reference should be removed as well
*/
function removeReference(name, opposite) {
const ref = this.references[name]
_.unset(this.references, name)
if (ref.opposite !== undefined) {
if (opposite) {
_.unset(ref.type.references, ref.opposite)
} else {
_.unset(ref.type.references[ref.opposite], 'opposite')
}
}
}
function populateClassFunction(cls) {
Object.defineProperties(cls,
{ getInheritanceChain: {value: getInheritanceChain}
, newInstance: {value: init => new cls(init)}
, /** Returns the superClasses of a Class
* @name Class~ClassInstance#conformsTo
*/
conformsTo: {value: () => conformsTo(cls)}
, getAllReferences: {value: getAllReferences}
, addReference: {value: addReference}
, removeReference: {value: removeReference}
, setReference: {value: addReference}
, addReferences: {value: addReferences}
, setReferences: {value: addReferences}
, getAllAttributes: {value: getAllAttributes}
, addAttribute: {value: addAttribute}
, removeAttribute: {value: removeAttribute}
, setAttribute: {value: addAttribute}
, addAttributes: {value: addAttributes}
, setAttributes: {value: addAttributes}
, setSuperType: {value: setSuperType}
, setSuperClass: {value: setSuperType}
, setSuperClasses: {value: setSuperType}
, setFlexible: {value: setFlexible}
})
}
/** Change superClasses of this class.
*
* @method
* @memberof Class~ClassInstance
* @param s - Either a {@link Class} or an array of {@link Class}
*/
function setSuperType(s) {
const ss = _.isArray(s) ? s : [s]
this.superClasses = _.uniq(this.superClasses.concat(ss))
}
/** Add an attribute to a class.
* @method
* @memberof Class~ClassInstance
* @param {string} name - The name of the attribute
* @param {Function} type - In jsmf an attribute Type is a function that returns true if the
* value is a member of this type, false otherwise. Some predefined
* types are available in the class {@link Type}.
* Users can also use builtin JavaScript types, that are replaced
* on the fly by the corresponding validation function
* @param {boolean} mandatory - If set to true, the attribute can't be set to undefined.
* @param {Function} errorCallback - defines what to do if an invalid value is set to this attribute.
*/
function addAttribute(name, type, mandatory, errorCallback) {
this.attributes[name] =
{ type: Type.normalizeType(type)
, mandatory: mandatory || false
, errorCallback: errorCallback || this.errorCallback
}
}
/** Remove an attribute to a class
* @method
* @memberof Class~ClassInstance
* @param {string} name - The name of the attribute
*/
function removeAttribute(name) {
_.unset(this.attributes, name)
}
/** Add several attributes
* @method
* @memberof Class~ClassInstance
* @param {Object} attrs - The attributes to add. The keys are te attribute name,
* the values are the attributes descriptors.
* See {@link Class~ClassInstance#RemoveAttribute} for
* the supported properties.
*/
function addAttributes(attrs) {
_.forEach(attrs, (v, k) => {
if (v.type !== undefined) {
this.addAttribute(k, v.type, v.mandatory, v.errorCallback)
} else {
this.addAttribute(k, v)
}
})
}
function createAttributes(e, cls) {
_.forEach(cls.getAllAttributes(), (desc, name) => {
e.__jsmf__.attributes[name] = undefined
createAttribute(e, name, desc)
})
}
function createReferences(e, cls) {
_.forEach(cls.getAllReferences(), (desc, name) => {
e.__jsmf__.associated[name] = []
e.__jsmf__.references[name] = []
createAddReference(e, name, desc)
createRemoveReference(e, name)
createReference(e, name, desc)
})
}
function createAttribute(o, name, desc) {
createSetAttribute(o,name, desc)
Object.defineProperty(o, name,
{ get: () => o.__jsmf__.attributes[name]
, set: o[setName(name)]
, enumerable: true
, configurable: true
}
)
}
function createSetAttribute(o, name, desc) {
Object.defineProperty(o, setName(name),
{ value: x => {
if (!desc.type(x) && (desc.mandatory || !_.isUndefined(x))) {
desc.errorCallback(o, name, x)
}
o.__jsmf__.attributes[name] = x
}
, configurable: true
})
}
/** Check whether a class (or some classes) are in the inheritance chain of a given class
* @function
* @param {Class} x - The class to test
* @param type - Either a {@link Class} or an array of classes tha should be in the class x.
*/
function hasClass(x, type) {
const types = _.isArray(type) ? type : [type]
return _.some(x.conformsTo().getInheritanceChain(), c => _.includes(types, c))
}
function createReference(o, name, desc) {
Object.defineProperty(o, name,
{ get: () => o.__jsmf__.references[name]
, set: xs => {
xs = _.isArray(xs) ? xs : [xs]
const invalid = _.filter(xs, x => {
const type = conformsTo(x)
return type === undefined
|| !(_.includes(type.getInheritanceChain(), desc.type) || (desc.type === Type.JSMFAny))
})
if (!_.isEmpty(invalid)) {
desc.errorCallback(o, name, xs)
}
o.__jsmf__.associated[name] = []
if (desc.opposite !== undefined) {
const removed = _.difference(o[name], xs)
_.forEach(removed, function(y) {
_.remove(y.__jsmf__.references[desc.opposite], z => z === o)
})
const added = _.difference(xs, o[name])
_.forEach(added, y => y.__jsmf__.references[desc.opposite].push(o))
}
o.__jsmf__.references[name] = xs
}
, enumerable: true
, configurable: true
})
}
function createAddReference(o, name, desc) {
Object.defineProperty(o, addName(name),
{ value: function(xs, associated) {
xs = _.isArray(xs) ? xs : [xs]
const associationMap = o.__jsmf__.associated
const backup = associationMap[name]
o.__jsmf__.references[name] = o[name].concat(xs)
if (desc.opposite !== undefined) {
_.forEach(xs, y => y.__jsmf__.references[desc.opposite].push(o))
}
associationMap[name] = backup
if (associated !== undefined) {
associationMap[name] = associationMap[name] || []
if (desc.associated !== undefined
&& !_.includes(associated.conformsTo().getInheritanceChain(), desc.associated)) {
throw new Error(`Invalid association ${associated} for object ${o}`)
}
_.forEach(xs, x => {
associationMap[name].push({elem: x, associated})
if (desc.opposite !== undefined) {
x.__jsmf__.associated[desc.opposite].push({elem: o, associated})
}
})
}
}
, configurable: true
})
}
function createRemoveReference(o, name) {
Object.defineProperty(o, removeName(name),
{ value: xs => {
xs = _.isArray(xs) ? xs : [xs]
const associationMap = o.__jsmf__.associated
associationMap[name] = _.differenceWith(associationMap[name], xs, (x,y) => x.elem === y)
o.__jsmf__.references[name] = _.difference(o.__jsmf__.references[name], xs)
}
, configurable: true
})
}
function classMeta() {
return {uuid: generateId(), conformsTo: Class}
}
/** Decide whether whether or not type will becheck for attributes and
* references of a whole class.
* @method
* @memberof Class~ClassInstance
* @param {boolean} b - If true, type is not checked on assignement,
* if false wrong assignement type riase an error.
*/
function setFlexible(b) {
this.errorCallback = b ? onError.silent : onError.throw
_.forEach(this.references, r => r.errorCallback = this.errorCallback)
_.forEach(this.attributes, r => r.errorCallback = this.errorCallback)
}
function elementMeta(constructor) {
return { conformsTo: constructor
, uuid: generateId()
, attributes: {}
, references: {}
, associated: {}
}
}
function addName(n) {
return prefixedName('add',n)
}
function setName(n) {
return prefixedName('set',n)
}
function removeName(n) {
return prefixedName('remove',n)
}
function prefixedName(pre, n) {
return pre + _.upperFirst(n)
}
function refreshElement(o) {
const mm = o.conformsTo()
if (!mm) {return o}
const oBackup = Object.assign({}, o)
for (let x in o) {delete o[x]}
createAttributes(o, mm)
createReferences(o, mm)
_.forEach(oBackup, (v, k) => {
if (v !== undefined) { o[k] = v }
})
return o
}
/** Predefined function for type error handling.
* It can be used in {@link Class~ClassInstance#addReference} and {@link Class~ClassInstance#addAttribute}
*/
const onError =
{ /** Raise an error on type error
* @member
*/
'throw': function(o,n,x) {throw new TypeError(`Invalid assignment: ${x} for property ${n} of object ${o}`)}
, /** Assign and log the error on type error
* @member
*/
'log': function(o,n,x) {console.log(`assignment: ${x} for property ${n} of object ${o}`)}
, /** Assign anyway.
* @member
*/
'silent': function() {}
}
module.exports = { Class, isJSMFClass, hasClass, onError, refreshElement }