├── .gitignore ├── .npmignore ├── test ├── persistence.json ├── Enum.js ├── Examples.js ├── Type.js ├── test_advanced.js ├── Model.js ├── JSMFEquality.js ├── Flexibility.js ├── ClassInstance.js └── Class.js ├── examples ├── ArduinoML │ ├── README.md │ ├── Switch.js │ └── MMArduinoML.js └── FamilyToPerson │ ├── Persons.js │ └── Families.js ├── .eslintrc.json ├── .gitlab-ci.yml ├── package.json ├── LICENSE ├── CHANGELOG ├── src ├── Type.js ├── Common.js ├── Enum.js ├── Cardinality.js ├── index.js ├── Model.js └── Class.js └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | 4 | -------------------------------------------------------------------------------- /test/persistence.json: -------------------------------------------------------------------------------- 1 | {"__name":"Reference","referenceModel":{},"modellingElements":{"State":[{"__name":"State","__attributes":{},"__references":{},"__superType":{}}]}} -------------------------------------------------------------------------------- /examples/ArduinoML/README.md: -------------------------------------------------------------------------------- 1 | # The ArduinoML Example 2 | 3 | This directory contains 2 javascript file: 4 | 5 | - `MMArduinoMl.js`, which implements the (ArduinoML model)[https://github.com/mosser/ArduinoML-kernel/tree/master/docs]; 6 | - `MArduinoML`, which implement the simple "switch" arduino app, described on the same document. 7 | -------------------------------------------------------------------------------- /examples/FamilyToPerson/Persons.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const JSMF = require('../../src/index') 4 | 5 | let Class, Model, Enum 6 | 7 | (function() { 8 | Model = JSMF.Model 9 | Class = JSMF.Class 10 | }).call() 11 | 12 | const Person = new Class('Person', [], {fullName: String}) 13 | const Male = new Class('Male', Person) 14 | const Female = new Class('Male', Person) 15 | 16 | const Persons = new Model('Persons', undefined, [Person, Male, Female]) 17 | 18 | module.exports = {Persons} 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 2 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "never" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | 3 | # Init ssh 4 | - eval $(ssh-agent -s) 5 | - ssh-add <(echo "$SSH_PRIVATE_KEY") 6 | - ssh-add -L 7 | - curl -sL https://deb.nodesource.com/setup_4.x | bash - 8 | 9 | # For Docker builds disable host key checking. Be aware that by adding that 10 | # you are suspectible to man-in-the-middle attacks. 11 | # WARNING: Use this only with the Docker executor, if you use it with shell 12 | # you will overwrite your user's SSH config. 13 | - mkdir -p ~/.ssh 14 | - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' 15 | 16 | - apt-get update -qq && apt-get install -y nodejs && npm install 17 | test: 18 | script: 19 | - npm test 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsmf-core", 3 | "version": "0.12.0", 4 | "description": "Javascript Modelling Framework", 5 | "main": "src/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha --recursive" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/JS-MF/jsmf-core.git" 15 | }, 16 | "keywords": [ 17 | "Modeling", 18 | "Model-Driven", 19 | "Engineering", 20 | "Model", 21 | "Transformation", 22 | "Model", 23 | "Management" 24 | ], 25 | "author": "Jean-Sebastien Sottet (http://www.list.lu/)", 26 | "license": "MIT", 27 | "dependencies": { 28 | "lodash": "^4.15.0", 29 | "uuid": "^2.0.3" 30 | }, 31 | "devDependencies": { 32 | "assert": "^1.3.0", 33 | "mocha": "^11.1.0", 34 | "should": "^11.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 LIST 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/FamilyToPerson/Families.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const JSMF = require('../../src/index') 4 | 5 | let Class, Model, Enum 6 | 7 | (function() { 8 | Model = JSMF.Model 9 | Class = JSMF.Class 10 | }).call() 11 | 12 | const Member = new Class('Member', [], {firstName : String}) 13 | const Family = new Class('Family', [], {lastName : String}, 14 | { father: { type: Member 15 | , cardinality: JSMF.Cardinality.one 16 | , opposite: 'familyFather' 17 | , oppositeCardinality: JSMF.Cardinality.optional 18 | } 19 | , mother: { type: Member 20 | , cardinality: JSMF.Cardinality.one 21 | , opposite: 'familyMother' 22 | , oppositeCardinality: JSMF.Cardinality.optional 23 | } 24 | , sons: { type: Member 25 | , cardinality: JSMF.Cardinality.any 26 | , opposite: 'familySon' 27 | , oppositeCardinality: JSMF.Cardinality.optional 28 | } 29 | , daughters: { type: Member 30 | , cardinality: JSMF.Cardinality.any 31 | , opposite: 'familyDaughter' 32 | , oppositeCardinality: JSMF.Cardinality.optional 33 | } 34 | }) 35 | 36 | const Families = new Model('Families', undefined, [Family, Member]) 37 | 38 | module.exports = {Families} 39 | -------------------------------------------------------------------------------- /examples/ArduinoML/Switch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AML = require('./MMArduinoML.js') 4 | const Model = require('../../src/index').Model 5 | 6 | 7 | const button = AML.Sensor.newInstance({name: 'button', pin: 9}) 8 | const led = AML.Actuator.newInstance({name: 'led', pin: 12}) 9 | 10 | /* 11 | * on state 12 | */ 13 | 14 | const aOn = AML.Action.newInstance({value: AML.Signal.HIGH, actuator: led}) 15 | const tOn = AML.Transition.newInstance({value: AML.Signal.HIGH, sensor: button}) 16 | 17 | const on = AML.State.newInstance({name: 'on'}) 18 | on.action = aOn 19 | on.transition = tOn 20 | 21 | /* 22 | * off state 23 | */ 24 | 25 | const aOff = AML.Action.newInstance({value: AML.Signal.LOW, actuator: led}) 26 | const tOff = AML.Transition.newInstance({value: AML.Signal.HIGH, sensor: button}) 27 | const off = AML.State.newInstance({name: 'off'}) 28 | off.action = aOff 29 | off.transition = tOff 30 | 31 | 32 | /* 33 | * set transitions 34 | */ 35 | tOn.next = off 36 | tOff.next = on 37 | 38 | 39 | /* 40 | * define app 41 | */ 42 | 43 | const switchApp = AML.App.newInstance({ 44 | name: 'Switch!', 45 | bricks: [button, led], 46 | states: [on, off], 47 | initial: off 48 | }) 49 | 50 | const Switch = new Model('Switch', AML.ArduinoML, switchApp, true) 51 | 52 | module.exports = { 53 | Switch: Switch, 54 | switchApp: switchApp 55 | } 56 | -------------------------------------------------------------------------------- /examples/ArduinoML/MMArduinoML.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const JSMF = require('../../src/index') 4 | 5 | let Class, Model, Enum 6 | 7 | (function() { 8 | Model = JSMF.Model 9 | Class = JSMF.Class 10 | Enum = JSMF.Enum 11 | }).call() 12 | 13 | 14 | const Signal = new Enum('Signal', ['LOW', 'HIGH']) 15 | 16 | const NamedElement = Class.newInstance('NamedElement', [], {name: String}) 17 | 18 | const App = Class.newInstance('App', NamedElement) 19 | 20 | const State = Class.newInstance('State', NamedElement) 21 | App.setReference('states', State, -1) 22 | App.setReference('initial', State, 1) 23 | 24 | const Brick = Class.newInstance('Brick', NamedElement, {pin: JSMF.Range(0,13)}) 25 | 26 | const Action = Class.newInstance('Action', [], {value: Signal}) 27 | State.setReference('action', Action, -1) 28 | 29 | const Transition = Class.newInstance('Transition', [], {value: Signal}) 30 | Transition.setReference('next', State, 1) 31 | State.setReference('transition', Transition, 1) 32 | 33 | const Sensor = Class.newInstance('Sensor', Brick) 34 | Transition.setReference('sensor', Sensor, 1) 35 | 36 | var Actuator = Class.newInstance('Actuator', Brick) 37 | Action.setReference('actuator', Actuator, 1) 38 | 39 | App.setReference('bricks', Brick, -1) 40 | 41 | var ArduinoML = new Model('ArduinoML', {}, App, true) 42 | 43 | module.exports = JSMF.modelExport(ArduinoML) 44 | -------------------------------------------------------------------------------- /test/Enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require("assert") 4 | , should = require('should') 5 | , JSMF = require('../src/index') 6 | , Enum = JSMF.Enum 7 | 8 | describe('Enum instance', function() { 9 | 10 | it('conforms to Enum', function(done) { 11 | var e = new Enum('TurnMe', ['on', 'off']) 12 | e.conformsTo().should.equal(Enum) 13 | done() 14 | }) 15 | 16 | it('is a JSMF element', function(done) { 17 | var e = new Enum('TurnMe', ['on', 'off']) 18 | JSMF.isJSMFElement(e).should.be.true() 19 | done() 20 | }) 21 | 22 | it('can be initialize with an Array of values', function(done) { 23 | var e = new Enum('TurnMe', ['on', 'off']) 24 | e.should.have.property('on', 0) 25 | e.should.have.property('off', 1) 26 | done() 27 | }) 28 | 29 | it('can be initialize with an object', function(done) { 30 | var e = new Enum('TurnMe', {on: 'jour', off: 'nuit'}) 31 | e.should.have.property('on', 'jour') 32 | e.should.have.property('off', 'nuit') 33 | done() 34 | }) 35 | 36 | it('can resolve keys from values', function(done) { 37 | var e = new Enum('TurnMe', ['on', 'off']) 38 | e.getName(0).should.equal('on') 39 | e.getName(1).should.equal('off') 40 | should(e.getName(2)).equal(undefined) 41 | done() 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.10.0: 2 | - We can "refresh" jsmf elements 3 | (synchronize the elements with the current version of the class) 4 | 0.9.2: 5 | - Fix issue for setter of camel case references 6 | - Fix issue when we crawl Classes with JSMFAny references 7 | 0.9.0: 8 | - Externalize Model conformance 9 | 0.8.0: 10 | Internal Changes: 11 | - Classes & Enums are now store under resp. the key "Class" and "Enum" 12 | - In Models, classes allows an access to Classes and Enum by their names 13 | - UUID are stored in their binary format (no longer as a string) 14 | 15 | 0.7.0: 16 | Features: 17 | - Models and Classes and Enums are now JSMF elements 18 | - Flexible Models 19 | - Model and element Conformance check 20 | 21 | 0.6.0: 22 | Internal: 23 | - Migration to ES6 24 | 25 | 0.5.0: 26 | 27 | Model: 28 | - Add an `elements` functions: list the modelling elements of the model 29 | whatever their classes are. 30 | - Add a `crop` function: removes refer4ences to elements that are not 31 | into the model. 32 | 33 | 0.4.0: 34 | 35 | Features: 36 | - Inline object definition 37 | - Custom types for attributes 38 | - Minimal Cardinality 39 | - Can use javascript assignation to set references 40 | - Can assign opposite reference directly 41 | - Can remove elements from references 42 | 43 | Internal: 44 | - Change JSMF objects internal representations 45 | -------------------------------------------------------------------------------- /test/Examples.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var JSMF = require('../src/index'); 5 | 6 | describe('ArduinoML', function() { 7 | 8 | describe('Metamodel', function() { 9 | 10 | var MM = require('../examples/ArduinoML/MMArduinoML'); 11 | 12 | it('exports the expected elements', function(done) { 13 | MM.should.have.properties( 14 | [ 'NamedElement' 15 | , 'App' 16 | , 'Brick' 17 | , 'Sensor' 18 | , 'Actuator' 19 | , 'State' 20 | , 'Transition' 21 | , 'Signal' 22 | , 'ArduinoML' 23 | ]); 24 | done(); 25 | }); 26 | 27 | it('has the expected references and attributes', function(done) { 28 | MM.NamedElement.attributes.should.have.property(['name']); 29 | MM.App.references.should.have.properties(['states', 'initial', 'bricks']); 30 | MM.State.references.should.have.properties(['action', 'transition']); 31 | done(); 32 | }); 33 | 34 | }); 35 | 36 | describe('Model', function() { 37 | 38 | var M = require('../examples/ArduinoML/Switch'); 39 | 40 | it('Model has the expected elements', function(done) { 41 | var me = M.Switch.modellingElements; 42 | me.should.have.properties( 43 | [ 'App' 44 | , 'Sensor' 45 | , 'Actuator' 46 | , 'State' 47 | , 'Transition' 48 | ]); 49 | me.App[0].should.have.properties(['initial', 'states']); 50 | me.App[0].states.should.have.length(2); 51 | done(); 52 | }); 53 | 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/Type.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | , JSMF = require('../src/index') 5 | 6 | describe('JSMFAny', function() { 7 | 8 | it('validates any JSMF class', done => { 9 | const a = new JSMF.Class('A') 10 | JSMF.JSMFAny(a).should.be.true() 11 | done() 12 | }) 13 | 14 | it('validates any JSMF element', done => { 15 | const A = new JSMF.Class('A') 16 | const a = new A() 17 | JSMF.JSMFAny(a).should.be.true() 18 | done() 19 | }) 20 | 21 | it('refuses non JSMF objects', done => { 22 | const a = {x:42} 23 | JSMF.JSMFAny(a).should.be.false() 24 | done() 25 | }) 26 | 27 | }) 28 | 29 | describe('Function', done => { 30 | 31 | it('accept functions', done => { 32 | JSMF.Function(x => x + 1).should.be.true() 33 | done() 34 | }) 35 | 36 | it('refuses objects', done => { 37 | JSMF.Function({x :42}).should.be.false() 38 | done() 39 | }) 40 | }) 41 | 42 | describe('Range', done => { 43 | 44 | it('accepts values in range', done => { 45 | const r = new JSMF.Range(0,4) 46 | r(2).should.be.true() 47 | done() 48 | }) 49 | 50 | it('accepts min value', done => { 51 | const r = new JSMF.Range(0,4) 52 | r(0).should.be.true() 53 | done() 54 | }) 55 | 56 | it('accepts max value', done => { 57 | const r = new JSMF.Range(0,4) 58 | r(4).should.be.true() 59 | done() 60 | }) 61 | 62 | it('rejects out of scope value', done => { 63 | const r = new JSMF.Range(0,4) 64 | r(5).should.be.false() 65 | done() 66 | }) 67 | 68 | }) 69 | 70 | describe('normalizeType', done => { 71 | 72 | it ('transform Number to JSMF.Number', done => { 73 | JSMF.normalizeType(Number).should.be.eql(JSMF.Number) 74 | JSMF.normalizeType(String)(2).should.be.false() 75 | done() 76 | }) 77 | 78 | }) 79 | -------------------------------------------------------------------------------- /src/Type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved 4 | * JavaScript Modelling Framework (JSMF) 5 | * 6 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 9 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 10 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 11 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 12 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 13 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 14 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 15 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 16 | * POSSIBILITY OF SUCH DAMAGE. 17 | * 18 | * @author J.S. Sottet 19 | * @author N. Biri 20 | * @author A. Vagner 21 | */ 22 | 23 | 'use strict' 24 | 25 | const _ = require('lodash') 26 | , conformsTo = (require('./Common')).conformsTo 27 | 28 | 29 | module.exports = 30 | { Number: _.isNumber 31 | , Positive: x => x >= 0 32 | , Negative: x => x <= 0 33 | , String: _.isString 34 | , Boolean: _.isBoolean 35 | , Date: _.isDate 36 | , Array: _.isArray 37 | , Object: _.isObject 38 | , Function: _.isFunction 39 | , Range: function Range(min, max) { 40 | const self = x => x >= min && x <= max 41 | Object.assign(self, {typeName: 'Range', min, max}) 42 | return self 43 | } 44 | , JSMFAny: x => conformsTo(x) !== undefined 45 | , Any: _.constant(true) 46 | } 47 | 48 | /** Transform a native JS type in a JSMF type */ 49 | module.exports.normalizeType = function normalizeType(t) { 50 | switch (t) { 51 | case Number: return module.exports.Number 52 | case String: return module.exports.String 53 | case Boolean: return module.exports.Boolean 54 | case Array: return module.exports.Array 55 | case Object: return module.exports.Object 56 | case Date: return module.exports.Date 57 | default: return t 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/Common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | , uuid = require('uuid') 5 | 6 | /** 7 | * @license 8 | * ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved 9 | * JavaScript Modelling Framework (JSMF) 10 | * 11 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 12 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 13 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 14 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 15 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 16 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 17 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 18 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 19 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 20 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 21 | * POSSIBILITY OF SUCH DAMAGE. 22 | * 23 | * @author J.S. Sottet 24 | * @author N. Biri 25 | * @author A. Vagner 26 | */ 27 | 28 | /** Provides the conformsTo relationship of any jsmfElement 29 | * @param o - A jsmf Object (usually Class, Enum, Model, or ClassInstance) 30 | */ 31 | function conformsTo(o) { 32 | return _.get(o, ['__jsmf__', 'conformsTo']) 33 | } 34 | 35 | /** Returns the jsmfId of any jsmfElement 36 | * @param o - A jsmf Object (usually Class, Enum, Model, or ClassInstance) 37 | */ 38 | function jsmfId(o) { 39 | return _.get(o, ['__jsmf__', 'uuid']) 40 | } 41 | 42 | /** Check if an object is a JSMFElement 43 | * By construction a JSMFElement has a jsmfID, 44 | * returns a value on {@link conformsTo} 45 | * this value has a getInheritanceChain method. 46 | */ 47 | function isJSMFElement(o) { 48 | const implement = conformsTo(o) 49 | return implement 50 | && _.get(implement, 'getInheritanceChain') !== undefined 51 | && jsmfId(o) !== undefined 52 | } 53 | 54 | function generateId() { 55 | const arrayUUID = new Array(16) 56 | uuid.v4(null, arrayUUID) 57 | return arrayUUID 58 | } 59 | 60 | module.exports = 61 | { conformsTo 62 | , jsmfId 63 | , isJSMFElement 64 | , generateId 65 | } 66 | -------------------------------------------------------------------------------- /src/Enum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved 4 | * JavaScript Modelling Framework (JSMF) 5 | * 6 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 9 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 10 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 11 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 12 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 13 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 14 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 15 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 16 | * POSSIBILITY OF SUCH DAMAGE. 17 | * 18 | * @author J.S. Sottet 19 | * @author N. Biri 20 | * @author A. Vagner 21 | */ 22 | 23 | 'use strict' 24 | 25 | const _ = require('lodash') 26 | 27 | let conformsTo, generateId 28 | (function () { 29 | const Common = require('./Common') 30 | conformsTo = Common.conformsTo 31 | generateId = Common.generateId 32 | }).call() 33 | 34 | /** Define an Enum 35 | * @constructor 36 | * @param {string} name - The name of the created Enum 37 | * @param values - Either an Array of string or an Object. 38 | * If an Array is provided, the indexes are used as Enum values. 39 | */ 40 | function Enum(name, values) { 41 | /** The generic Enum instance 42 | * @constructor 43 | */ 44 | function EnumInstance(x) {return _.includes(EnumInstance, x)} 45 | Object.defineProperties(EnumInstance, 46 | { __jsmf__: {value: {uuid: generateId(), conformsTo: Enum}} 47 | , __name: {value: name} 48 | , getName: {value: getName} 49 | , conformsTo: {value: () => conformsTo(EnumInstance)} 50 | }) 51 | if (_.isArray(values)) { 52 | _.forEach(values, (v, k) => EnumInstance[v] = k) 53 | } else { 54 | _.forEach(values, (v, k) => EnumInstance[k] = v) 55 | } 56 | return EnumInstance 57 | } 58 | 59 | /** The Enum name */ 60 | Enum.__name = 'Enum' 61 | 62 | 63 | Enum.getInheritanceChain = () => [Enum] 64 | 65 | /** Given a value, find the associated key in an Enum, if any. 66 | * @memberof Enum~EnumInstance 67 | */ 68 | function getName(o) { 69 | return _.findKey(this, v => v === o) 70 | } 71 | 72 | /** Check if an object is an Enum 73 | */ 74 | function isJSMFEnum(o) { 75 | return conformsTo(o) === Enum 76 | } 77 | 78 | module.exports = {Enum, isJSMFEnum} 79 | -------------------------------------------------------------------------------- /src/Cardinality.js: -------------------------------------------------------------------------------- 1 | 'use srtict' 2 | const _ = require('lodash') 3 | 4 | /** 5 | * @license 6 | * ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved 7 | * JavaScript Modelling Framework (JSMF) 8 | * 9 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 10 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 11 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 12 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 13 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 14 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 15 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 16 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 17 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 18 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 19 | * POSSIBILITY OF SUCH DAMAGE. 20 | * 21 | * @author J.S. Sottet 22 | * @author N. Biri 23 | * @author A. Vagner 24 | */ 25 | 26 | /** Reference Cardinality 27 | * @constructor 28 | * @param {number} min - The lower bound of a cardinality 29 | * @param {number} max - The upper bound of a cardinality 30 | */ 31 | function Cardinality(min, max) { 32 | /** The minimal cardinality, will be consider as 0 if undefined 33 | */ 34 | this.min = min 35 | /** The maxmial cardinality, if udefined, there is no max (*) 36 | */ 37 | this.max = max 38 | } 39 | 40 | /** A Shortcut for 0..1 cardinality 41 | */ 42 | Cardinality.optional = new Cardinality(0,1) 43 | 44 | /** A Shortcut for 1..1 cardinality 45 | */ 46 | Cardinality.one = new Cardinality(1,1) 47 | 48 | /** A Shortcut for 0..* cardinality 49 | */ 50 | Cardinality.any = new Cardinality(0) 51 | 52 | /** A Shortcut for * cardinality 53 | */ 54 | Cardinality.some = new Cardinality(1) 55 | 56 | /** Build a Cardinality from a value. 57 | * If the value is a positive number v, the result will be 0..v 58 | * If it's any other value, we'll try to get the min and max properties 59 | * and use respectively 0 and undefined if they are not set. 60 | * 61 | * @example 62 | * // returns {min: 0, max: 4} 63 | * Cardinality.check(4) 64 | * 65 | * @example 66 | * // returns {min: 2, max: 4} 67 | * Cardinality.check({min: 2, max: 4}) 68 | * 69 | * @example 70 | * // returns {min: 2, max: undefined} 71 | * Cardinality.check({min: 2, foo: 4}) 72 | * 73 | * @example 74 | * // returns {min: 0, max: undefined} 75 | * Cardinality.check(undefined) 76 | */ 77 | Cardinality.check = function(v) { 78 | return (_.isNumber(v) && v >= 0) 79 | ? {min: 0, max: v} 80 | : {min: _.get(v, 'min', 0), max: _.get(v, 'max', undefined)} 81 | } 82 | 83 | module.exports = {Cardinality} 84 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved 4 | * JavaScript Modelling Framework (JSMF) 5 | * 6 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 9 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 10 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 11 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 12 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 13 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 14 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 15 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 16 | * POSSIBILITY OF SUCH DAMAGE. 17 | * 18 | * @author J.S. Sottet 19 | * @author N. Biri 20 | * @author A. Vagner 21 | */ 22 | 23 | 'use strict' 24 | 25 | const _ = require('lodash') 26 | 27 | const Common = require('./Common') 28 | const Model = require('./Model') 29 | const Class = require('./Class') 30 | const Enum = require('./Enum') 31 | const Cardinality = require('./Cardinality') 32 | const Type = require('./Type') 33 | 34 | function customizer(obj, other) { 35 | if (obj === other) {return true} 36 | if (Common.isJSMFElement(obj) && Common.isJSMFElement(obj)) { 37 | if (!jsmfIsEqual(obj.conformsTo(), other.conformsTo())) { 38 | return false 39 | } 40 | if (Enum.isJSMFEnum(obj) && Enum.isJSMFEnum(other)) { 41 | return jsmfIsEqual(dryEnum(obj), dryEnum(other)) 42 | } else if (Class.isJSMFClass(obj) && Class.isJSMFClass(other)) { 43 | return jsmfIsEqual(dryClass(obj), dryClass(other)) 44 | } else if (obj instanceof Model.Model && obj instanceof Model.Model) { 45 | return jsmfIsEqual(dryModel(obj), dryModel(other)) 46 | } else { 47 | return jsmfIsEqual(dryElement(obj), dryElement(other)) 48 | } 49 | } 50 | } 51 | 52 | function dryEnum(e) { 53 | const res = _.toPairsIn(e) 54 | return {__jsmf: {uuid: Common.jsmfId(e)}, values: res, name: e.__name} 55 | } 56 | 57 | function dryClass(c) { 58 | return _.assign({__jsmf: {uuid: Common.jsmfId(c)}}, _.toPairsIn(_.omit(c, 'errorCallback'))) 59 | } 60 | 61 | function dryElement(e) { 62 | return _.assign({__jsmf: {uuid: Common.jsmfId(e), conformsTo: e.conformsTo()}}, _.toPairsIn(e)) 63 | } 64 | 65 | function dryModel(c) { 66 | return _.assign({__jsmf: {uuid: Common.jsmfId(c)}}, _.pick(c, ['__name', 'referenceModel', 'modellingElements'])) 67 | } 68 | 69 | /** Check structural equality for JSMF elements. 70 | */ 71 | function jsmfIsEqual(obj, other) { 72 | return _.isEqualWith(obj, other, customizer) 73 | } 74 | 75 | module.exports = _.assign( 76 | {jsmfIsEqual}, 77 | Common, 78 | Model, 79 | Class, 80 | Enum, 81 | Cardinality, 82 | Type) 83 | 84 | -------------------------------------------------------------------------------- /test/test_advanced.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require("assert"); 4 | var should = require('should'); 5 | var JSMF = require('../src/index'); 6 | var Class = JSMF.Class; 7 | var Model = JSMF.Model; 8 | 9 | describe('Create Dynamic Instances', function() { 10 | 11 | describe('Create Instance from metamodel references', function(){ 12 | //WARNING the references must be set AFTER the creation of Classes 13 | it('Instance created from reference', function(done){ 14 | 15 | var Transition = Class.newInstance('Transition'); 16 | Transition.setAttribute('active', Boolean); 17 | 18 | var Property = Class.newInstance('Property'); 19 | Property.setAttribute('blink', Number); 20 | 21 | var State = Class.newInstance('State'); 22 | State.setAttribute('name', String); 23 | State.setAttribute('id', Number); 24 | 25 | State.setReference('transition', Transition, -1); 26 | State.setReference('property', Property, 1); 27 | 28 | var s1 = State.newInstance('s1'); 29 | var tabOfInstance = {}; 30 | for(var i in State.references) { 31 | var Type = State.references[i].type; 32 | tabOfInstance[Type.__name]=Type.newInstance(); 33 | } 34 | 35 | tabOfInstance['Transition'].should.have.property('setActive'); 36 | tabOfInstance['Property'].should.have.property('setBlink'); 37 | var t1 = tabOfInstance['Transition']; 38 | var p1 = tabOfInstance['Property']; 39 | t1.should.have.property('setActive'); 40 | should(t1.active).be.empty; 41 | t1.setActive(true); 42 | t1.should.have.property('active',true); 43 | 44 | should(p1.blink).be.empty; 45 | p1.setBlink(182); 46 | p1.should.have.property('blink',182); 47 | 48 | done(); 49 | }) 50 | 51 | it('Instance created inherited reference', function(done){ 52 | var Transition = Class.newInstance('Transition'); 53 | Transition.setAttribute('active', Boolean); 54 | 55 | var Property = Class.newInstance('Property'); 56 | Property.setAttribute('blink', Number); 57 | 58 | var SuperState = Class.newInstance('SuperState'); 59 | SuperState.setReference('property',Property, 1); 60 | 61 | var State = Class.newInstance('State'); 62 | State.setAttribute('name', String); 63 | State.setAttribute('id', Number); 64 | 65 | State.setReference('transition', Transition, -1); 66 | State.setSuperType(SuperState); 67 | 68 | var s1 = State.newInstance('s1'); 69 | var tabOfInstance = {}; 70 | for(var i in State.references) { 71 | var Type = State.references[i].type; 72 | tabOfInstance[Type.__name]=Type.newInstance(); 73 | } 74 | 75 | tabOfInstance['Transition'].should.have.property('setActive'); 76 | var t1 = tabOfInstance['Transition']; 77 | t1.should.have.property('setActive'); 78 | should(t1.active).be.empty; 79 | t1.setActive(true); 80 | t1.should.have.property('active',true); 81 | 82 | done(); 83 | }) 84 | 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # JSMF-Core 2 | 3 | [![build status](https://git.list.lu/jsmf/jsmf-core/badges/master/build.svg)](https://git.list.lu/jsmf/jsmf-core/commits/master) 4 | 5 | The JavaScript Modelling Framework (JSMF) has been designed for providing a flexible modelling environment that could support the many modelling situations: from informal model to code generation. It is a JavaScript embedded DSL inspired by the Eclipse Modelling Framework (EMF) in its basic functions but that rely on JavaScript dynamic typing and on a relative independence between a metamodel and a model. 6 | 7 | ## Install 8 | 9 | Thanks to npm: `npm install jsmf-core` 10 | 11 | ## Usage and Example 12 | 13 | ### In order to access the most commun JSMF elements use: 14 | ```javascript 15 | const JSMF = require('jsmf-core') 16 | Model = JSMF.Model 17 | Class = JSMF.Class 18 | Enum = JSMF.Enum 19 | ``` 20 | 21 | ### Creating a metamodel element (i.e., Class) 22 | 23 | Here we created a Class "Family" and a "lastname" attribute. 24 | This syntax is inspired by the EMF one. 25 | ```javascript 26 | const Course = Class.newInstance('Course') 27 | Course.addAttribute('title' , String ) 28 | 29 | const Person = Class.newInstance('Person') 30 | Person.addAttribute('age' , JSMF.Positive) 31 | ``` 32 | Alternatively you can use a more compact syntax (here defining a Person Class) using javascript objects. 33 | ```javascript 34 | const Student = Class.newInstance ('Student', [] , 35 | { firstname : String , lastName : String}) 36 | ``` 37 | You can use the different basic Javascript types : Number, String. Boolean, BigInt(*), Date, Array, Objects. There are also some constrained types like JSMF.Positive and JSMF.Negative that takes respectively only positive and negative numbers. A range of value can also be defined using Range(Min,Max). Finally, for a flexible usage, you can use JSMF.Any that is not enforcing any specific type checking. 38 | 39 | Inheritance is managed using the function setSuperClass, this way multiple classes can be added using an array like: [Person,Human]. SetSuperClasses or SetSuperTypes are also alternative syntax. 40 | ```javascript 41 | Student.setSuperClass(Person); 42 | ``` 43 | 44 | Inheritance can also be specified when creating the class student: 45 | ```javascript 46 | const Student = Class.newInstance ('Student', [Person]) 47 | ``` 48 | 49 | 50 | Having defined the classes "Person" and "Family" let's create a reference between those two classes. 51 | 52 | ```javascript 53 | Family.addReference ('registeredStudents', Student , JSMF.Cardinality.Some ) 54 | ``` 55 | The cardinality (or multiplicities) indicates the number of instances of one class can be linked to the instances of another class under a given reference. There is a minimum and a maximum (which can be potentially infinite indicated by *) 56 | Shortcuts are following, note that using using the value -1 also mean 0..* 57 | ```javascript 58 | /** A Shortcut for 0..1 cardinality 59 | */ 60 | Cardinality.optional = new Cardinality(0,1) 61 | 62 | /** A Shortcut for 1..1 cardinality 63 | */ 64 | Cardinality.one = new Cardinality(1,1) 65 | 66 | /** A Shortcut for 0..* cardinality 67 | */ 68 | Cardinality.any = new Cardinality(0) 69 | 70 | /** A Shortcut for * cardinality 71 | */ 72 | Cardinality.some = new Cardinality(1) 73 | ``` 74 | 75 | ### Creating a model element conforms to a metamodel element. 76 | 77 | 78 | ```javascript 79 | const john = new Student () 80 | john . firstname = ’ John ’ 81 | john . age = 46 82 | ``` 83 | 84 | Alternatively you can also use a compact syntax: 85 | ```javascript 86 | const Modelling = Course . newInstance ({ name : ’ Modelling ’ , 87 | registeredStudents : [ john ]}) 88 | ``` 89 | 90 | You can find examples, discover the other components and test it online with Tonic on JSMF github website (https://js-mf.github.io/#portfolio) 91 | 92 | ## License information 93 | 94 | See [License](LICENSE). 95 | -------------------------------------------------------------------------------- /test/Model.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | , _ = require('lodash') 5 | , JSMF = require('../src/index') 6 | , Model = JSMF.Model 7 | , Class = JSMF.Class 8 | 9 | describe('Model', function() { 10 | 11 | describe('constructor', function() { 12 | 13 | it('creates an empty model', function(done) { 14 | const foo = new Model('Foo') 15 | foo.modellingElements.should.be.empty() 16 | foo.referenceModel.should.be.empty() 17 | done() 18 | }) 19 | 20 | it('creates a model that is a jsmf element', function(done) { 21 | const foo = new Model('Foo') 22 | JSMF.isJSMFElement(foo).should.be.true() 23 | done() 24 | }) 25 | 26 | it ('creates a model with a given reference model', function(done) { 27 | const MM = new Model('Foo') 28 | const M = new Model('Bar', MM) 29 | M.referenceModel.should.equal(MM) 30 | done() 31 | }) 32 | 33 | it ('adds the given classes to the model', function(done) { 34 | const A = new Class('A') 35 | const B = new Class('B') 36 | const M = new Model('Bar', {}, [A, B]) 37 | M.modellingElements.should.have.property('Class', [A, B]) 38 | M.classes.should.have.property('A', [A]) 39 | M.classes.should.have.property('B', [B]) 40 | done() 41 | }) 42 | 43 | it ('adds the given elements to the model', function(done) { 44 | const A = new Class('A') 45 | const a = new A() 46 | const B = new Class('B') 47 | const b = new B() 48 | const M = new Model('Bar', {}, [a, b]) 49 | M.modellingElements.should.have.property('A', [a]) 50 | M.modellingElements.should.have.property('B', [b]) 51 | done() 52 | }) 53 | 54 | it ('does not add the given elements transitively if not mentioned', function(done) { 55 | const A = new Class('A') 56 | const B = new Class('B') 57 | A.addReference('b', B) 58 | const a = new A() 59 | const b = new B() 60 | a.b = b 61 | const M = new Model('Bar', {}, [a]) 62 | M.modellingElements.should.have.property('A', [a]) 63 | M.modellingElements.should.not.have.property('B') 64 | done() 65 | }) 66 | 67 | it ('adds the given elements transitively if mentioned', function(done) { 68 | const A = new Class('A') 69 | const B = new Class('B') 70 | A.addReference('b', B) 71 | const a = new A() 72 | const b = new B() 73 | a.b = b 74 | const M = new Model('Bar', {}, [a], true) 75 | M.modellingElements.should.have.property('A', [a]) 76 | M.modellingElements.should.have.property('B', [b]) 77 | done() 78 | }) 79 | 80 | it ('adds class to te model and set the model flexible, should propagate to all classes', function(done) { 81 | const A = new Class('A') 82 | const B = new Class('B') 83 | const M = new Model('Bar', {}, [A, B]) 84 | M.modellingElements.should.have.property('Class', [A, B]) 85 | M.setFlexible(true) 86 | M.classes['A'][0].isFlexible().should.equal(true) 87 | M.classes['B'][0].isFlexible().should.equal(true) 88 | done() 89 | }) 90 | 91 | }) 92 | 93 | describe('elements', function() { 94 | it('lists all the elements of the model', function(done) { 95 | const A = new Class('A') 96 | const B = new Class('B') 97 | A.addReference('b', B) 98 | const a = new A() 99 | const b = new B() 100 | a.b = b 101 | const M = new Model('Bar', {}, [a, b], true) 102 | const elements = M.elements() 103 | elements.should.have.length(2) 104 | elements.should.containEql(a) 105 | elements.should.containEql(b) 106 | done() 107 | }) 108 | }) 109 | 110 | describe('crop', function() { 111 | 112 | it('remove references to elements that are not in the model', function(done) { 113 | const A = new Class('A') 114 | const B = new Class('B') 115 | A.addReference('b', B) 116 | const a = new A() 117 | const b = new B() 118 | a.b = b 119 | const M = new Model('Bar', {}, [a], true) 120 | M.crop() 121 | a.b.should.be.empty() 122 | done() 123 | }) 124 | 125 | it('remove references to elements that are not in the model', function(done) { 126 | const A = new Class('A') 127 | const B = new Class('B') 128 | A.addReference('b', B, 1, 'back', 1, A) 129 | const a = new A() 130 | const b = new B() 131 | a.addB(b, a) 132 | const M = new Model('Bar', {}, [a], true) 133 | M.crop() 134 | a.b.should.be.empty() 135 | a.getAssociated().b.should.be.empty() 136 | done() 137 | }) 138 | 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /test/JSMFEquality.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | , jsmf = require('../src/index') 5 | 6 | describe ('jsmf equals on Enum', () => { 7 | 8 | it ('returns true on the exact same Enum', done => { 9 | const e = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 10 | jsmf.jsmfIsEqual(e,e).should.be.true() 11 | done() 12 | }) 13 | 14 | it ('returns true on Enum with the same name and values', done => { 15 | const e = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 16 | const e2 = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 17 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 18 | jsmf.jsmfIsEqual(e,e2).should.be.true() 19 | done() 20 | }) 21 | 22 | it ('returns false on Enum with different uuid', done => { 23 | const e = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 24 | const e2 = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 25 | jsmf.jsmfIsEqual(e,e2).should.be.false() 26 | done() 27 | }) 28 | 29 | it ('returns false on Enum with the same name and different values', done => { 30 | const e = new jsmf.Enum('Foo', ['test', 'is', 'not', 'ok']) 31 | const e2 = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 32 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 33 | jsmf.jsmfIsEqual(e,e2).should.be.false() 34 | done() 35 | }) 36 | 37 | it ('returns false on Enum with different name and same values', done => { 38 | const e = new jsmf.Enum('Foo', ['test', 'is', 'ok']) 39 | const e2 = new jsmf.Enum('Bar', ['test', 'is', 'ok']) 40 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 41 | jsmf.jsmfIsEqual(e,e2).should.be.false() 42 | done() 43 | }) 44 | }) 45 | 46 | describe ('jsmf equals on Class', () => { 47 | 48 | it ('returns true on the exact same Class', done => { 49 | const r = new jsmf.Class('Target') 50 | const e = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: r}) 51 | jsmf.jsmfIsEqual(e,e).should.be.true() 52 | done() 53 | }) 54 | 55 | it ('returns true on Classes with the same elements', done => { 56 | const r = new jsmf.Class('Target') 57 | const r2 = new jsmf.Class('Target') 58 | const e = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: r}) 59 | const e2 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: r2}) 60 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 61 | r2.__jsmf__.uuid = jsmf.jsmfId(r) 62 | jsmf.jsmfIsEqual(e,e2).should.be.true() 63 | done() 64 | }) 65 | 66 | it ('returns false on Classes with different names', done => { 67 | const e = new jsmf.Class('Bar', [], {foo: jsmf.Positive}) 68 | const e2 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}) 69 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 70 | jsmf.jsmfIsEqual(e,e2).should.be.false() 71 | done() 72 | }) 73 | 74 | it ('returns false on Classes with different attributes', done => { 75 | const e = new jsmf.Class('Foo', [], {foo: jsmf.Positive}) 76 | const e2 = new jsmf.Class('Foo', [], {bar: jsmf.Positive}) 77 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 78 | jsmf.jsmfIsEqual(e,e2).should.be.false() 79 | done() 80 | }) 81 | 82 | it ('returns false on Classes with different attributes types', done => { 83 | const e = new jsmf.Class('Foo', [], {foo: jsmf.Positive}) 84 | const e2 = new jsmf.Class('Foo', [], {bar: jsmf.String}) 85 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 86 | jsmf.jsmfIsEqual(e,e2).should.be.false() 87 | done() 88 | }) 89 | 90 | it ('returns false on Classes with different references', done => { 91 | const r = new jsmf.Class('Target') 92 | const e = new jsmf.Class('Foo', [], {}, {ref: r}) 93 | const e2 = new jsmf.Class('Foo', [], {}, {ref2: r}) 94 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 95 | jsmf.jsmfIsEqual(e,e2).should.be.false() 96 | done() 97 | }) 98 | 99 | it ('returns true on cyclic structure', done => { 100 | const r = new jsmf.Class('Target') 101 | const r2 = new jsmf.Class('Target') 102 | const e = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: r}) 103 | r.addReference('back', e) 104 | const e2 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: r2}) 105 | r2.addReference('back', e2) 106 | e2.__jsmf__.uuid = jsmf.jsmfId(e) 107 | r2.__jsmf__.uuid = jsmf.jsmfId(r) 108 | jsmf.jsmfIsEqual(e,e2).should.be.true() 109 | done() 110 | }) 111 | 112 | }) 113 | 114 | describe ('jsmf equals on Class instances', () => { 115 | 116 | 117 | it ('returns true on similar instances', done => { 118 | const R = new jsmf.Class('Target') 119 | const C0 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: R}) 120 | const C1 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: R}) 121 | C1.__jsmf__.uuid = jsmf.jsmfId(C0) 122 | const r = new R() 123 | const c0 = new C0({foo: 42, ref: r}) 124 | const c1 = new C1({foo: 42, ref: r}) 125 | c1.__jsmf__.uuid = jsmf.jsmfId(c0) 126 | jsmf.jsmfIsEqual(c0,c1).should.be.true() 127 | done() 128 | }) 129 | }) 130 | 131 | describe ('jsmf equals on Model', () => { 132 | 133 | it ('ensures that a model is equal to itself', done => { 134 | const m = new jsmf.Model('Foo') 135 | jsmf.jsmfIsEqual(m,m).should.be.true() 136 | done() 137 | }) 138 | 139 | it ('ensures that a model is equal to identic model', done => { 140 | const R = new jsmf.Class('Target') 141 | const C0 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: R}) 142 | const C1 = new jsmf.Class('Foo', [], {foo: jsmf.Positive}, {ref: R}) 143 | C1.__jsmf__.uuid = jsmf.jsmfId(C0) 144 | const r = new R() 145 | const c0 = new C0({foo: 42, ref: r}) 146 | const c1 = new C1({foo: 42, ref: r}) 147 | const m0 = new jsmf.Model('Foo', [], c0, true) 148 | const m1 = new jsmf.Model('Foo', [], c1, true) 149 | c1.__jsmf__.uuid = jsmf.jsmfId(c0) 150 | m1.__jsmf__.uuid = jsmf.jsmfId(m0) 151 | jsmf.jsmfIsEqual(m0,m1).should.be.true() 152 | done() 153 | }) 154 | 155 | }) 156 | -------------------------------------------------------------------------------- /test/Flexibility.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | , _ = require('lodash') 5 | , JSMF = require('../src/index') 6 | , Class = JSMF.Class 7 | 8 | describe('Class Flexibility', () => { 9 | 10 | describe('errorCallback', () => { 11 | 12 | describe('throw callback', () => { 13 | 14 | it('throws error on invalid attribute value', done => { 15 | const A = new Class('A', [], {x: {type: Number, errorCallback: JSMF.onError.throw}}) 16 | let test = function() {new A({x: 'foo'})} 17 | test.should.throw() 18 | done() 19 | }) 20 | 21 | it('throws error on invalid reference value', done => { 22 | const A = new Class('A', []) 23 | const B = new Class('B', []) 24 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.throw) 25 | let test = function() {let x = new A(); let y = new B(); x.x = y} 26 | test.should.throw() 27 | done() 28 | }) 29 | 30 | it('throws error on invalid reference value', done => { 31 | const A = new Class('A', []) 32 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.throw) 33 | let test = function() {let x = new A(); x.x = 'toto'} 34 | test.should.throw() 35 | done() 36 | }) 37 | 38 | it('throws error on invalid attribute assignation', done => { 39 | const A = new Class('A', [], {val: Number}) 40 | const a = new A({val: 12}) 41 | a.val.should.be.equal(12) 42 | function test() {a.bval = 42} 43 | test.should.throw() 44 | done() 45 | }) 46 | 47 | }) 48 | 49 | describe('silent callback', () => { 50 | 51 | it('assigns on invalid attribute value', done => { 52 | const A = new Class('A', [], {x: {type: Number, errorCallback: JSMF.onError.silent}}) 53 | let x = new A({x: 12}) 54 | x.x = 'toto' 55 | x.x.should.be.equal('toto') 56 | done() 57 | }) 58 | 59 | 60 | it('assigns on invalid reference value', done => { 61 | const A = new Class('A', []) 62 | const B = new Class('B', []) 63 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.silent) 64 | let x = new A() 65 | x.x = x 66 | let y = new B() 67 | x.x = y 68 | x.x.should.be.eql([y]) 69 | done() 70 | }) 71 | 72 | it('assign invalid reference value', done => { 73 | const A = new Class('A', []) 74 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.silent) 75 | let x = new A() 76 | x.x = x 77 | x.x = 'toto' 78 | x.x.should.be.eql(['toto']) 79 | done() 80 | }) 81 | 82 | }) 83 | 84 | describe('default behaviour', () => { 85 | 86 | it('throws on invalid attribute value', done => { 87 | const A = new Class('A', [], {x: Number}) 88 | let test = function() {new A({x: 'foo'})} 89 | test.should.throw() 90 | done() 91 | }) 92 | 93 | it('throws on invalid reference value', done => { 94 | const A = new Class('A', []) 95 | const B = new Class('B', []) 96 | A.addReference('x', A, 1) 97 | let test = function() {let x = new A(); let y = new B(); x.x = y} 98 | test.should.throw() 99 | done() 100 | }) 101 | 102 | }) 103 | 104 | describe('change flexibility behaviour to strict', () => { 105 | 106 | it('throws on invalid attribute value', done => { 107 | const A = new Class('A', [], {x: {type: Number, errorCallback: JSMF.onError.silent}}) 108 | A.setFlexible(false) 109 | let test = function() {new A({x: 'foo'})} 110 | test.should.throw() 111 | done() 112 | }) 113 | 114 | it('throws on invalid attribute assignation', done => { 115 | const A = new Class('A', [], {x:Number},[],true) 116 | const a = new A(); 117 | a.z = 12 118 | a.z.should.equal(12) 119 | A.setFlexible(false) 120 | let test = function() { a.b='toto'} 121 | test.should.throw() 122 | done() 123 | }) 124 | 125 | it('throws on invalid reference value', done => { 126 | const A = new Class('A', []) 127 | const B = new Class('B', []) 128 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.silent) 129 | A.setFlexible(false) 130 | let test = function() {let x = new A(); let y = new B(); x.x = y} 131 | test.should.throw() 132 | done() 133 | }) 134 | 135 | }) 136 | 137 | describe('change flexibility behaviour to flexible', () => { 138 | 139 | it('assigns on invalid attribute value', done => { 140 | const A = new Class('A', [], {x: {type: Number, errorCallback: JSMF.onError.throw}}) 141 | A.setFlexible(true) 142 | let x = new A({x: 12}) 143 | x.x = 'toto' 144 | x.x.should.be.equal('toto') 145 | done() 146 | }) 147 | 148 | it('assigns on invalid attribute value', done => { 149 | const A = new Class('A', [], {x: {type: Number, errorCallback: JSMF.onError.throw}}) 150 | const a = new A({x: 12}) 151 | function test() {a.bval = 42} 152 | test.should.throw() 153 | A.setFlexible(true) 154 | a.bval=42 155 | a.bval.should.be.equal(42) 156 | done() 157 | }) 158 | 159 | it('assigns on invalid reference value', done => { 160 | const A = new Class('A', []) 161 | const B = new Class('B', []) 162 | A.addReference('x', A, 1, undefined, undefined, undefined, JSMF.onError.throw) 163 | A.setFlexible(true) 164 | let x = new A() 165 | x.x = x 166 | let y = new B() 167 | x.x = y 168 | x.x.should.be.eql([y]) 169 | done() 170 | }) 171 | 172 | }) 173 | }) 174 | 175 | describe('changing the metamodel', () => { 176 | 177 | it('ensures that removed property is not acessible via getAllAttributes', done => { 178 | const A = new Class('A', [], {a: Number}) 179 | const a = new A({a: 12}) 180 | delete A.attributes['a'] 181 | _.pick(a, _.keys(A.getAllAttributes())).should.eql({}) 182 | done() 183 | }) 184 | 185 | it('ensures that added property is acessible via getAllAttributes', done => { 186 | const A = new Class('A', [], {a: Number}) 187 | const a = new A({a: 12}) 188 | A.setFlexible(true); 189 | a.b = 'foo' 190 | _.pick(a, _.keys(A.getAllAttributes())).should.eql({a: 12}) 191 | delete A.addAttribute('b', String) 192 | _.pick(a, _.keys(A.getAllAttributes())).should.eql({a: 12, b: 'foo'}) 193 | done() 194 | }) 195 | 196 | it('throws error on a new attribute wrong type when refresh', done => { 197 | const A = new Class('A', [], {a: Number}) 198 | const a = new A({a: 12}) 199 | delete A.addAttribute('b', String) 200 | JSMF.refreshElement(a) 201 | function test() {a.b = 42} 202 | test.should.throw() 203 | done() 204 | }) 205 | 206 | it('except if the class is flexible', done => { 207 | const A = new Class('A', [], {a: Number}) 208 | const a = new A({a: 12}) 209 | delete A.addAttribute('b', String) 210 | JSMF.refreshElement(a) 211 | function test() {a.b = 42} 212 | test.should.throw() 213 | done() 214 | }) 215 | 216 | 217 | it ('adds flexible attributes to a flexible class', done => { 218 | const A = new Class('A', [], {a: Number}, {}, true) 219 | const a = new A({a: 12}) 220 | delete A.addAttribute('b', String) 221 | JSMF.refreshElement(a) 222 | function test() {a.b = 42} 223 | test.should.not.throw() 224 | done() 225 | }) 226 | 227 | it ('adds flexible references to a flexible class', done => { 228 | const A = new Class('A', [], {a: Number}, {}, true) 229 | const B = new Class('B') 230 | const a = new A({a: 12}) 231 | delete A.addReference('b', B) 232 | JSMF.refreshElement(a) 233 | function test() {a.b = new A()} 234 | test.should.not.throw() 235 | function testAddFunction() {a.addB(new A())} 236 | testAddFunction.should.not.throw() 237 | done() 238 | }) 239 | 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /src/Model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ©2015-2025 Luxembourg Institute of Science and Technology All Rights Reserved 4 | * JavaScript Modelling Framework (JSMF) 5 | * 6 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 9 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 10 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 11 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 12 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 13 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 14 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 15 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 16 | * POSSIBILITY OF SUCH DAMAGE. 17 | * 18 | * @author J.S. Sottet 19 | * @author N. Biri 20 | * @author A. Vagner 21 | */ 22 | 23 | 'use strict' 24 | 25 | const _ = require('lodash') 26 | 27 | let isJSMFElement, conformsTo, generateId 28 | (function() { 29 | const common = require('./Common') 30 | isJSMFElement = common.isJSMFElement 31 | conformsTo = common.conformsTo 32 | generateId = common.generateId 33 | }).call() 34 | 35 | let isJSMFClass, refreshElement 36 | (function() { 37 | const C = require('./Class') 38 | isJSMFClass = C.isJSMFClass 39 | refreshElement = C.refreshElement 40 | }).call() 41 | 42 | const isJSMFEnum = require('./Enum').isJSMFEnum 43 | 44 | /** A Model is basically a set of elements 45 | * @constructor 46 | * @param {string} name - The model name 47 | * @param {Model} referenceModel - The reference model (indicative and used for conformance) 48 | * @param {Class~ClassInstance[]} modellingElements - Either an element or an array of elements that 49 | * should be included in the model 50 | * @param {boolean} transitive - If false, only the elements in modellingElements will be in the model, 51 | * otherwise all theelements that can be reach from modellingElements 52 | * references are included in the model 53 | */ 54 | function Model(name, referenceModel, modellingElements, transitive) { 55 | /** @memberOf Model 56 | * @member {string} name - The name of the model 57 | */ 58 | this.__name = name 59 | _.set(this, ['__jsmf__','conformsTo'], Model) 60 | _.set(this, ['__jsmf__','uuid'], generateId()) 61 | /** @memberof Model 62 | * @member {Model} referenceModel - The referenceModel (an empty object if undefined) 63 | */ 64 | this.referenceModel = referenceModel || {} 65 | /** @memberof Model 66 | * @member {Object} modellingElements - The objects of the model, classified by their class name 67 | */ 68 | this.modellingElements = {} 69 | /** @memberOf Model 70 | * @member {Object} classes - classes and enums of the model, classified by their name 71 | */ 72 | this.classes = {} 73 | if (modellingElements !== undefined) { 74 | modellingElements = _.isArray(modellingElements) ? modellingElements : [modellingElements] 75 | if (transitive) { 76 | modellingElements = crawlElements(modellingElements) 77 | } 78 | _.forEach(modellingElements, e => this.addModellingElement(e)) 79 | } 80 | } 81 | 82 | /** A helper to export a model and all its classes in a npm module. 83 | */ 84 | function modelExport(m) { 85 | const result = _.mapValues(m.classes, _.head) 86 | result[m.__name] = m 87 | return result 88 | } 89 | 90 | /** Add a modelling element to a model 91 | * @param es - Either an element or an array of elements. 92 | */ 93 | Model.prototype.addModellingElement = function(es) { 94 | es = _.isArray(es) ? es : [es] 95 | _.forEach(es, e => { 96 | if (!isJSMFElement(e)) {throw new TypeError(`can't Add ${e} to model ${this}`)} 97 | addToClass(this, e) 98 | addToModellingElements(this, e) 99 | }) 100 | } 101 | 102 | function addToClass(m, e) { 103 | if (isJSMFClass(e) || isJSMFEnum(e)) { 104 | const key = e.__name 105 | const current = m.classes[key] || [] 106 | current.push(e) 107 | m.classes[key] = current 108 | } 109 | } 110 | 111 | function addToModellingElements(m, e) { 112 | const key = conformsTo(e).__name 113 | const current = m.modellingElements[key] || [] 114 | current.push(e) 115 | m.modellingElements[key] = current 116 | } 117 | 118 | /** @deprecated 119 | * @method 120 | */ 121 | Model.prototype.Filter = function(cls) { 122 | return this.modellingElements[_.get(cls, '__name')] 123 | } 124 | 125 | /** Model inheritanceChain 126 | * @method 127 | */ 128 | Model.getInheritanceChain = _.constant([Model]) 129 | Model.prototype.conformsTo = function() { return conformsTo(this) } 130 | 131 | /** @deprecated 132 | * @method 133 | * @see Model#addModellingElement 134 | */ 135 | Model.prototype.setModellingElements = Model.prototype.addModellingElement 136 | 137 | /** @see Model#addModellingElement 138 | * @method 139 | */ 140 | Model.prototype.add = Model.prototype.addModellingElement 141 | 142 | /** Set the reference model of a model 143 | */ 144 | Model.prototype.setReferenceModel = function(rm) {this.referenceModel = rm} 145 | 146 | /** List all the elements of a Model 147 | */ 148 | Model.prototype.elements = function() { 149 | return _(this.modellingElements).values().flatten().value() 150 | } 151 | 152 | /** 153 | * @ flexible : Boolean, true if all containing classes should be set flexible, false otherwise 154 | * Warning should also turn some assignation options on/off 155 | */ 156 | Model.prototype.setFlexible = function(flexible) { 157 | var metaclasses = this.modellingElements.Class 158 | metaclasses.forEach(x => { x.setFlexible(flexible)}) 159 | } 160 | 161 | /** 162 | * Remove from the elements of a model all the elements that are not explicitely in the model 163 | */ 164 | Model.prototype.crop = function() { 165 | const elements = this.elements() 166 | _.forEach(elements, e => { 167 | const mme = e.conformsTo() 168 | if (mme !== undefined) { 169 | for (var refName in mme.references) { 170 | e.__jsmf__.references[refName] = _.intersection(e.__jsmf__.references, elements) 171 | e.__jsmf__.associated[refName] = _.filter(e.__jsmf__.associated[refName], 172 | x => _.includes(e.__jsmf__.references[refName], x.elem)) 173 | } 174 | } 175 | }) 176 | } 177 | 178 | 179 | function crawlElements(init) { 180 | const visited = new Set() 181 | let toVisit = init 182 | while (!_.isEmpty(toVisit)) { 183 | var e = toVisit.pop() 184 | if (!visited.has(e)) { 185 | visited.add(e) 186 | let newNodes 187 | if (isJSMFClass(e)) { 188 | const refs = e.getAllReferences() 189 | const refTypes = _(refs).map('type').filter(isJSMFClass).value() 190 | const attrs = e.getAllAttributes() 191 | const attrsEnum = _(attrs).values().map('type').filter(isJSMFEnum).value() 192 | newNodes = _.flatten([refTypes, attrsEnum, e.getInheritanceChain()]) 193 | } else if (isJSMFEnum(e)) { 194 | newNodes = [] 195 | } else if (isJSMFElement(e)) { 196 | const refs = conformsTo(e).getAllReferences() 197 | const associated = _(e.getAssociated()).values().flatten().map('associated').value() 198 | newNodes = _(refs).map((v, x) => e[x]).flatten().value() 199 | newNodes = newNodes.concat(associated) 200 | } 201 | toVisit = toVisit.concat(newNodes) 202 | } 203 | } 204 | return dispatch(Array.from(visited)) 205 | } 206 | 207 | function dispatch(elems) { 208 | return _.reduce( 209 | elems, 210 | (acc, e) => { 211 | const key = (isJSMFClass(e) || isJSMFEnum(e)) ? e.__name : e.conformsTo().__name 212 | const values = _.get(acc, key, []) 213 | values.push(e) 214 | acc[key] = values 215 | return acc 216 | }, 217 | {}) 218 | } 219 | 220 | /** Update all the elements of a model: If the class they are conformsTo has changed, 221 | * update the elements to reflect these changes. 222 | */ 223 | function refreshModel(o) { 224 | if (!(o instanceof Model)) {throw new TypeError('Model expected')} 225 | _.forEach(o.elements(), refreshElement) 226 | } 227 | 228 | 229 | module.exports = {Model, modelExport, refreshModel} 230 | -------------------------------------------------------------------------------- /test/ClassInstance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | const JSMF = require('../src/index') 5 | const Class = JSMF.Class 6 | 7 | const State = new Class('State', [], {name: JSMF.String}) 8 | const Transition = new Class('Transition', [], {on: JSMF.String}) 9 | State.addReference('transitions', Transition, JSMF.Cardinality.any, 'source') 10 | Transition.addReference('next', State, JSMF.Cardinality.one) 11 | 12 | describe('Class instance', function() { 13 | 14 | describe('Class creation', function() { 15 | 16 | it('can be created using the class as a constructor', function(done) { 17 | const s = new State() 18 | s.conformsTo().should.equal(State) 19 | done() 20 | }) 21 | 22 | it('can be created using newInstance', function(done) { 23 | const s = State.newInstance() 24 | s.conformsTo().should.equal(State) 25 | done() 26 | }) 27 | 28 | it('has an UUID', function(done) { 29 | const s = State.newInstance() 30 | should(JSMF.jsmfId(s)).not.equal(undefined) 31 | done() 32 | }) 33 | 34 | it('creates attributes', function(done) { 35 | const s = State.newInstance() 36 | s.should.has.property('name') 37 | done() 38 | }) 39 | 40 | it('creates inherited attributes', function(done) { 41 | const MyState = new Class('MyState', State) 42 | const s = MyState.newInstance() 43 | s.should.has.property('name') 44 | done() 45 | }) 46 | 47 | it('creates references', function(done) { 48 | const s = State.newInstance() 49 | s.should.has.property('transitions') 50 | done() 51 | }) 52 | 53 | it('creates inherited references', function(done) { 54 | const MyState = new Class('MyState', State) 55 | const s = MyState.newInstance() 56 | s.should.has.property('transitions') 57 | done() 58 | }) 59 | 60 | it('can initialize attributes during creation', function(done) { 61 | const s = State.newInstance({name: 's0'}) 62 | s.should.has.property('name', 's0') 63 | done() 64 | }) 65 | 66 | it('can initialize references during creation', function(done) { 67 | const s = State.newInstance() 68 | const t = State.newInstance({next: [s]}) 69 | t.should.has.property('next', [s]) 70 | done() 71 | }) 72 | 73 | }) 74 | 75 | describe('attribute settings', function() { 76 | 77 | it('assign valid values', function(done) { 78 | const s = new State() 79 | s.name = 'foo' 80 | s.should.have.property('name', 'foo') 81 | done() 82 | }) 83 | 84 | it('can use setter syntax to set attributes', function(done) { 85 | const s = new State() 86 | s.setName('foo') 87 | s.should.have.property('name', 'foo') 88 | done() 89 | }) 90 | 91 | it('throws error on invalid values', function(done) { 92 | const s = new State() 93 | function test() {s.name = 42} 94 | test.should.throw() 95 | done() 96 | }) 97 | 98 | it('throws error when we set undefined value for mandatory attribute', function(done) { 99 | const Foo = new Class('Foo', [], {x: {type: Number, mandatory: true}}); 100 | const s = new Foo({x: 12}) 101 | function test() {s.x = undefined} 102 | test.should.throw() 103 | done() 104 | }) 105 | 106 | it('allows undefined value to optional attribute', function(done) { 107 | const Foo = new Class('Foo', [], {x: {type: Number, mandatory: false}}); 108 | const s = new Foo({x: 12}) 109 | function test() {s.x = undefined} 110 | test.should.not.throw() 111 | done() 112 | }) 113 | 114 | it('doesn\'t stop on false value', function(done) { 115 | const C = new Class('C', undefined, {a: Boolean, b: Number}) 116 | const c = new C({a: false, b: 12}) 117 | c.should.have.property('b', 12) 118 | done() 119 | }) 120 | 121 | it('assign wongly typed values on relaxed schema', function(done) { 122 | const Foo = new Class('Foo', [], {test: {type: Number, errorCallback: JSMF.onError.silent}}) 123 | const s = new Foo({test: "i'm not a number"}) 124 | s.should.have.property('test', "i'm not a number") 125 | done() 126 | }) 127 | 128 | }) 129 | 130 | describe('reference settings', function() { 131 | 132 | it('assign valid values', function(done) { 133 | const s = new State() 134 | const t = new Transition() 135 | s.transitions = t 136 | s.should.have.property('transitions', [t]) 137 | done() 138 | }) 139 | 140 | it('throws error on invalid values', function(done) { 141 | const s = new State() 142 | const t = new Transition() 143 | function test () {s.transitions = s} 144 | test.should.throw() 145 | done() 146 | }) 147 | 148 | it('assign wongly typed values on relaxed schema', function(done) { 149 | const Foo = new Class('Foo', [], {}, {test: {target: State, errorCallback: JSMF.onError.silent}}) 150 | const s = new Foo() 151 | s.test = s 152 | s.should.have.property('test', [s]) 153 | done() 154 | }) 155 | 156 | it('replace former references when we use assignement', function(done) { 157 | const s = new State() 158 | const t1 = new Transition() 159 | const t2 = new Transition() 160 | s.transitions = [t1] 161 | s.transitions = t2 162 | s.should.have.property('transitions', [t2]) 163 | done() 164 | }) 165 | 166 | it('accept any object for a reference that has the targetType JSMFAny', function(done) { 167 | const Foo = new Class('Foo', [], {}, {test: {target: JSMF.JSMFAny}}) 168 | const Bar = new Class('Bar') 169 | const x = new Foo() 170 | const y = new Bar() 171 | x.test = [x,y] 172 | x.should.have.property('test', [x, y]) 173 | done() 174 | }) 175 | 176 | it('accept any object for a reference with no target type specified', function(done) { 177 | const Foo = new Class('Foo', [], {}, {test: {}}) 178 | const Bar = new Class('Bar') 179 | const x = new Foo() 180 | const y = new Bar() 181 | x.test = [x,y] 182 | x.should.have.property('test', [x, y]) 183 | done() 184 | }) 185 | 186 | it('add references when we use the adder', function(done) { 187 | const s = new State() 188 | const t1 = new Transition() 189 | const t2 = new Transition() 190 | s.addTransitions([t1]) 191 | s.addTransitions(t2) 192 | s.should.have.property('transitions', [t1, t2]) 193 | done() 194 | }) 195 | 196 | it('can remove elements from references with remove', function(done) { 197 | const s = new State() 198 | const t1 = new Transition() 199 | const t2 = new Transition() 200 | s.addTransitions([t1, t2]) 201 | s.removeTransitions(t2) 202 | s.should.have.property('transitions', [t1]) 203 | done() 204 | }) 205 | 206 | it ('adds elements to opposite relation', function(done) { 207 | const s = new State() 208 | const t1 = new Transition() 209 | const t2 = new Transition() 210 | s.addTransitions([t1, t2]) 211 | t1.should.have.property('source', [s]) 212 | t2.should.have.property('source', [s]) 213 | done() 214 | }) 215 | 216 | it ('assigns elements from opposite relation', function(done) { 217 | const s = new State() 218 | const t1 = new Transition() 219 | const t2 = new Transition() 220 | t1.source = s 221 | t2.source = s 222 | s.should.have.property('transitions', [t1, t2]) 223 | done() 224 | }) 225 | 226 | it ('allows the definition of associated data', function(done) { 227 | const s = new State() 228 | const t1 = new Transition() 229 | s.addTransitions(t1, "Associated data") 230 | s.getAssociated('transitions').should.eql([{elem: t1, associated: "Associated data"}]) 231 | done() 232 | }) 233 | 234 | it ('adds the associated data to the opposite reference', function(done) { 235 | const s = new State() 236 | const t1 = new Transition() 237 | s.addTransitions(t1, "Associated data") 238 | t1.getAssociated('source').should.eql([{elem: s, associated: "Associated data"}]) 239 | done() 240 | }) 241 | 242 | it ('reject associated data of the wrong type', function(done) { 243 | const s = new State() 244 | const T = new JSMF.Class('T') 245 | T.addReference('test', State, JSMF.Cardinality.any, undefined, undefined, State) 246 | const t1 = new T() 247 | t1.addTest(s, s) 248 | t1.getAssociated('test').should.eql([{elem: s, associated: s}]) 249 | done() 250 | }) 251 | 252 | it ('reject associated data of the wrong type', function(done) { 253 | const s = new State() 254 | const T = new JSMF.Class('T') 255 | T.addReference('test', State, JSMF.Cardinality.any, undefined, undefined, State) 256 | const t1 = new T() 257 | function test() {t1.addTest(s, t1)} 258 | test.should.throw() 259 | done() 260 | }) 261 | 262 | it ('creates correct setter names for camel case fields', function(done) { 263 | const s = new State() 264 | const T = new JSMF.Class('T') 265 | T.addReference('testCamel', State, JSMF.Cardinality.any, undefined, undefined, State) 266 | const t1 = new T() 267 | should(t1.addTestCamel).not.be.undefined() 268 | done() 269 | }) 270 | 271 | }) 272 | 273 | }) 274 | -------------------------------------------------------------------------------- /test/Class.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const should = require('should') 4 | , JSMF = require('../src/index') 5 | , Class = JSMF.Class 6 | 7 | describe('Class', function() { 8 | 9 | 10 | describe('Class construction', function() { 11 | 12 | it('should have the given name', function(done) { 13 | var Instance = new Class('Instance') 14 | Instance.__name.should.equal('Instance') 15 | done() 16 | }) 17 | 18 | it('should ConformsTo Class', function(done) { 19 | var Instance = new Class('Instance') 20 | Instance.conformsTo().should.equal(Class) 21 | done() 22 | }) 23 | 24 | it('is a JSMF element', function(done) { 25 | var Instance = new Class('Instance') 26 | JSMF.isJSMFElement(Instance).should.be.true() 27 | done() 28 | }) 29 | 30 | it('has an UUID', function(done) { 31 | var s = new Class('S') 32 | should(JSMF.jsmfId(s)).not.equal(undefined) 33 | done() 34 | }) 35 | 36 | it('can be built with newInstance', function(done) { 37 | var Instance = Class.newInstance('Instance') 38 | Instance.__name.should.equal('Instance') 39 | done() 40 | }) 41 | }) 42 | 43 | describe('Class inheritance', function() { 44 | 45 | it('should work with assigniation of superClasses', function(done) { 46 | var Instance = new Class('Instance') 47 | var SuperInstance = new Class('SuperInstance') 48 | var NonRelatedInstance = new Class('NonRelatedInstance') 49 | Instance.superClasses = [SuperInstance] 50 | Instance.superClasses.should.containEql(SuperInstance) 51 | done() 52 | }), 53 | 54 | it('should work with push of new superClass', function(done) { 55 | var Instance = new Class('Instance') 56 | var SuperInstance = new Class('SuperInstance') 57 | var NonRelatedInstance = new Class('NonRelatedInstance') 58 | Instance.superClasses.push(SuperInstance) 59 | Instance.superClasses.should.containEql(SuperInstance) 60 | done() 61 | }), 62 | 63 | it('should work at class initialization', function(done) { 64 | var SuperInstance = new Class('SuperInstance') 65 | var Instance = new Class('Instance', SuperInstance) 66 | var NonRelatedInstance = new Class('NonRelatedInstance') 67 | Instance.superClasses = [SuperInstance] 68 | Instance.superClasses.should.containEql(SuperInstance) 69 | done() 70 | }), 71 | 72 | it('can be done in cascade', function(done) { 73 | var Instance = new Class('Instance') 74 | var SuperInstance = new Class('SuperInstance') 75 | var SuperSuperInstance = new Class('SuperSuperInstance') 76 | Instance.superClasses = [SuperInstance] 77 | SuperInstance.superClasses = [SuperSuperInstance] 78 | Instance.superClasses.should.containEql(SuperInstance) 79 | Instance.superClasses.should.not.containEql(SuperSuperInstance) 80 | SuperInstance.superClasses.should.containEql(SuperSuperInstance) 81 | SuperInstance.superClasses.should.not.containEql(SuperInstance) 82 | done() 83 | }), 84 | 85 | it('should support multi-inheritance', function(done) { 86 | var SuperInstance = new Class('SuperInstance') 87 | var OtherSuperInstance = new Class('OtherSuperInstance') 88 | var Instance = new Class('Instance', [SuperInstance, OtherSuperInstance]) 89 | Instance.superClasses.should.have.length(2) 90 | Instance.superClasses.should.containEql(SuperInstance) 91 | Instance.superClasses.should.containEql(OtherSuperInstance) 92 | done() 93 | }) 94 | }) 95 | 96 | describe('Class attributes', function() { 97 | 98 | it('allows jsmf primitive types', function(done) { 99 | var State = new Class('State') 100 | State.attributes.should.be.empty 101 | State.setAttribute('name', JSMF.String) 102 | State.attributes.should.have.property('name') 103 | State.attributes.name.should.have.property('type', JSMF.String) 104 | State.attributes.name.should.have.property('mandatory', false) 105 | done() 106 | }) 107 | 108 | it('allows normalize built-in primitive types', function(done) { 109 | var State = new Class('State') 110 | State.attributes.should.be.empty() 111 | State.setAttribute('name', String) 112 | State.attributes.should.have.property('name') 113 | State.attributes.name.should.have.property('type', JSMF.String) 114 | State.attributes.name.should.have.property('mandatory', false) 115 | done() 116 | }) 117 | 118 | it('allows mandatory attributes', function(done) { 119 | var State = new Class('State') 120 | State.attributes.should.be.empty() 121 | State.setAttribute('name', String, true) 122 | State.attributes.should.have.property('name') 123 | State.attributes.name.should.have.property('type', JSMF.String) 124 | State.attributes.name.should.have.property('mandatory', true) 125 | done() 126 | }) 127 | 128 | it('has addAttribute is a synonym to setAttribute', function(done) { 129 | var State = new Class('State') 130 | State.attributes.should.be.empty() 131 | State.addAttribute('name', JSMF.String) 132 | State.attributes.should.have.property('name') 133 | State.attributes.name.should.have.property('type', JSMF.String) 134 | State.attributes.name.should.have.property('mandatory', false) 135 | done() 136 | }) 137 | 138 | it('accept bulk setting', function(done) { 139 | var State = new Class('State') 140 | State.attributes.should.be.empty 141 | State.addAttributes({name: {type: JSMF.String, mandatory: true}, age: Number}) 142 | State.attributes.should.have.property('name') 143 | State.attributes.name.should.have.property('type', JSMF.String) 144 | State.attributes.name.should.have.property('mandatory', true) 145 | State.attributes.should.have.property('age') 146 | State.attributes.age.should.have.property('type', JSMF.Number) 147 | State.attributes.age.should.have.property('mandatory', false) 148 | done() 149 | }) 150 | 151 | it('can be modified', function(done) { 152 | var State = new Class('State') 153 | State.attributes.should.be.empty 154 | State.addAttributes({name: JSMF.Number}) 155 | State.addAttributes({name: JSMF.String}) 156 | State.attributes.should.have.property('name') 157 | State.attributes.name.should.have.property('type', JSMF.String) 158 | State.attributes.name.should.have.property('mandatory', false) 159 | done() 160 | }) 161 | 162 | it('can be set at initialization', function(done) { 163 | var State = new Class('State', [], {name: JSMF.String, age: Number}) 164 | State.attributes.should.have.property('name') 165 | State.attributes.name.should.have.property('type', JSMF.String) 166 | State.attributes.name.should.have.property('mandatory', false) 167 | State.attributes.should.have.property('age') 168 | State.attributes.age.should.have.property('type', JSMF.Number) 169 | State.attributes.age.should.have.property('mandatory', false) 170 | done() 171 | }) 172 | 173 | it ('can be removed', function(done) { 174 | var State = new Class('State', [], {name: JSMF.String, age: Number}) 175 | State.removeAttribute('name') 176 | State.attributes.should.not.have.property('name') 177 | State.attributes.should.have.property('age') 178 | State.attributes.age.should.have.property('type', JSMF.Number) 179 | State.attributes.age.should.have.property('mandatory', false) 180 | done() 181 | }) 182 | 183 | }) 184 | 185 | describe('Class references', function() { 186 | 187 | it('can be assign', function(done) { 188 | var Hero = new Class('Hero') 189 | var SuperPower = new Class('SuperPower') 190 | Hero.setReference('has', SuperPower, JSMF.Cardinality.one) 191 | Hero.references.should.have.property('has') 192 | Hero.references.has.should.have.property('type', SuperPower) 193 | Hero.references.has.should.have.property('cardinality') 194 | Hero.references.has.cardinality.should.have.property('min' , 1) 195 | Hero.references.has.cardinality.should.have.property('max' , 1) 196 | done() 197 | }) 198 | 199 | it('has a default cardinality of {0,n}', function(done) { 200 | var Hero = new Class('Hero') 201 | var SuperPower = new Class('SuperPower') 202 | Hero.setReference('has', SuperPower) 203 | Hero.references.should.have.property('has') 204 | Hero.references.has.should.have.property('cardinality') 205 | Hero.references.has.cardinality.should.have.property('min', 0) 206 | Hero.references.has.cardinality.should.have.property('max', undefined) 207 | done() 208 | }) 209 | 210 | it('accepts only the max cardinality', function(done) { 211 | var Hero = new Class('Hero') 212 | var SuperPower = new Class('SuperPower') 213 | Hero.setReference('has', SuperPower, 2) 214 | Hero.references.should.have.property('has') 215 | Hero.references.has.should.have.property('cardinality') 216 | Hero.references.has.cardinality.should.have.property('max', 2) 217 | done() 218 | }) 219 | 220 | it('has unlimited max cardinality on negative number', function(done) { 221 | var Hero = new Class('Hero') 222 | var SuperPower = new Class('SuperPower') 223 | Hero.setReference('has', SuperPower, -1) 224 | Hero.references.should.have.property('has') 225 | Hero.references.has.should.have.property('cardinality') 226 | Hero.references.has.cardinality.should.have.property('max', undefined) 227 | done() 228 | }) 229 | 230 | it('push opposite reference in the target class', function(done) { 231 | var Hero = new Class('Hero') 232 | var SuperPower = new Class('SuperPower') 233 | Hero.setReference('has', SuperPower, JSMF.Cardinality.any, 'owners') 234 | SuperPower.references.should.have.property('owners') 235 | Hero.references.has.should.have.property('opposite', 'owners') 236 | done() 237 | }) 238 | 239 | it('keep the cardinality of the opposite ref if it exists', function(done) { 240 | const Hero = new Class('Hero') 241 | const SuperPower = new Class('SuperPower') 242 | SuperPower.setReference('owners', Hero, JSMF.Cardinality.one) 243 | Hero.setReference('has', SuperPower, JSMF.Cardinality.any, 'owners') 244 | SuperPower.references.should.have.property('owners') 245 | SuperPower.references.owners.cardinality.should.have.properties({min: 1, max: 1}) 246 | SuperPower.references.owners.should.have.property('opposite', 'has') 247 | done() 248 | }) 249 | 250 | it('support bulk definition', function(done) { 251 | var Hero = new Class('Hero') 252 | var SuperPower = new Class('SuperPower') 253 | Hero.setReferences({has: {target: SuperPower}, weakness: {target: SuperPower}}) 254 | Hero.references.should.have.property('has') 255 | Hero.references.should.have.property('weakness') 256 | done() 257 | }) 258 | 259 | it('can be set at initialization', function(done) { 260 | var SuperPower = new Class('SuperPower') 261 | var Hero = new Class('Hero', [], {}, {has: {target: SuperPower}, weakness: {target: SuperPower}}) 262 | Hero.references.should.have.property('has') 263 | Hero.references.should.have.property('weakness') 264 | done() 265 | }) 266 | 267 | it('has addReference as a synonym', function(done) { 268 | var Hero = new Class('Hero') 269 | var SuperPower = new Class('SuperPower') 270 | Hero.addReference('has', SuperPower, JSMF.Cardinality.one) 271 | Hero.references.should.have.property('has') 272 | Hero.references.has.should.have.property('type', SuperPower) 273 | Hero.references.has.should.have.property('cardinality') 274 | Hero.references.has.cardinality.should.have.property('min' , 1) 275 | Hero.references.has.cardinality.should.have.property('max' , 1) 276 | done() 277 | }) 278 | 279 | it ('can be removed', function(done) { 280 | var SuperPower = new Class('SuperPower') 281 | var Hero = new Class('Hero', [], {}, {has: {target: SuperPower}, weakness: {target: SuperPower}}) 282 | Hero.removeReference('has') 283 | Hero.references.should.not.have.property('has') 284 | Hero.references.should.have.property('weakness') 285 | done() 286 | }) 287 | 288 | it('remove a removed reference from its opposite', function(done) { 289 | var Hero = new Class('Hero') 290 | var SuperPower = new Class('SuperPower') 291 | Hero.setReference('has', SuperPower, JSMF.Cardinality.any, 'owners') 292 | Hero.removeReference('has') 293 | SuperPower.references.owners.should.not.have.property('opposite') 294 | done() 295 | }) 296 | 297 | it('can removed a reference and its opposite simulteneaously', function(done) { 298 | var Hero = new Class('Hero') 299 | var SuperPower = new Class('SuperPower') 300 | Hero.setReference('has', SuperPower, JSMF.Cardinality.any, 'owners') 301 | Hero.removeReference('has', true) 302 | SuperPower.references.should.not.have.property('owners') 303 | done() 304 | }) 305 | 306 | }) 307 | 308 | describe('Class getInheritanceChain', function() { 309 | 310 | it('returns the class alone if no superClasses', function(done) { 311 | var Hero = new Class('Hero') 312 | Hero.getInheritanceChain().should.be.eql([Hero]) 313 | done() 314 | }) 315 | 316 | it('returns the class and it\'s parent for simple inheritance', function(done) { 317 | var Hero = new Class('Hero') 318 | var SuperHero = new Class('SuperHero', [Hero]) 319 | SuperHero.getInheritanceChain().should.be.eql([Hero, SuperHero]) 320 | done() 321 | }) 322 | 323 | it('returns the class and it\'s parent for multiple inheritance', function(done) { 324 | var A = new Class('A') 325 | var B = new Class('B', [A]) 326 | var C = new Class('C', [A]) 327 | var D = new Class('D', [B,C]) 328 | D.getInheritanceChain().should.be.eql([A, B, A, C, D]) 329 | done() 330 | }) 331 | 332 | }) 333 | 334 | describe('Class getAllAttributes', function() { 335 | 336 | it('returns the attributes of the class if no inheritance', function(done) { 337 | var State = new Class('State') 338 | State.attributes.should.be.empty 339 | State.addAttributes({name: JSMF.String, age: Number}) 340 | const res = State.getAllAttributes() 341 | res.should.have.property('name') 342 | res.name.should.have.property('type', JSMF.String) 343 | res.name.should.have.property('mandatory', false) 344 | res.should.have.property('age') 345 | res.age.should.have.property('type', JSMF.Number) 346 | res.age.should.have.property('mandatory', false) 347 | done() 348 | }) 349 | 350 | it('get parents attributes, override them if necessary', function(done) { 351 | const State = new Class('State') 352 | const TargetState = new Class('TargetState', State) 353 | State.attributes.should.be.empty() 354 | State.addAttributes({name: JSMF.String, age: Number}) 355 | function positive(x) {return x > 0;} 356 | TargetState.addAttributes({age: positive, value: JSMF.Any}) 357 | const res = TargetState.getAllAttributes() 358 | res.should.have.property('name') 359 | res.name.should.have.property('type', JSMF.String) 360 | res.name.should.have.property('mandatory', false) 361 | res.should.have.property('age') 362 | res.age.should.have.property('type', positive) 363 | res.age.should.have.property('mandatory', false) 364 | res.should.have.property('value') 365 | res.value.should.have.property('type', JSMF.Any) 366 | res.value.should.have.property('mandatory', false) 367 | done() 368 | }) 369 | 370 | it ('kept the attributes of the last inherited class', function(done) { 371 | var A = new Class('A', [], {foo: JSMF.Number}) 372 | var B = new Class('B', [], {foo: JSMF.String}) 373 | var C = new Class('C', [A, B]) 374 | const res = C.getAllAttributes() 375 | res.should.have.property('foo') 376 | res.foo.should.have.property('type', JSMF.String) 377 | res.foo.should.have.property('mandatory', false) 378 | done() 379 | }) 380 | 381 | }) 382 | 383 | describe('Class getAllReferences', function() { 384 | 385 | it('returns the references of the class if no inheritance', done => { 386 | var State = new Class('State') 387 | State.attributes.should.be.empty 388 | State.addReference('next', State) 389 | State.getAllReferences().should.have.property('next') 390 | done() 391 | }) 392 | 393 | it('get parents references, override them if necessary', done => { 394 | var State = new Class('State') 395 | var TargetState = new Class('TargetState', State) 396 | State.attributes.should.be.empty() 397 | State.addAttributes({name: JSMF.String, age: Number}) 398 | State.addReference('next', State) 399 | TargetState.addReference('next', TargetState) 400 | var references = TargetState.getAllReferences() 401 | references.should.have.property('next') 402 | references.next.type.should.eql(TargetState) 403 | done() 404 | }) 405 | 406 | it ('kept the references of the last inherited class', done => { 407 | const A = new Class('A') 408 | A.addReference('foo', A) 409 | const B = new Class('B') 410 | B.addReference('foo', B) 411 | const C = new Class('C', [A, B]) 412 | const references = C.getAllReferences() 413 | references.should.have.property('foo') 414 | references.foo.type.should.eql(B) 415 | done() 416 | }) 417 | }) 418 | 419 | describe('Class description', function() { 420 | it('create the description when building the Class', done => { 421 | var Bus = new Class('Bus',[],[],[],false,'A standard city bus','https://schema.org/BusOrCoach') 422 | Bus.__description.should.equal('A standard city bus') 423 | Bus.getDescription().should.equal('A standard city bus') 424 | done() 425 | }) 426 | 427 | it('set the class description by setDescription function and retrives it', done => { 428 | var Bus = new Class('Bus') 429 | Bus.getSemanticReference.should.be.empty 430 | Bus.setDescription('Standard city bus representation') 431 | Bus.getDescription().should.equal('Standard city bus representation') 432 | done() 433 | }) 434 | }) 435 | 436 | describe('Class semanticReference', function() { 437 | it('create the semantic references (URI) building the Class', done => { 438 | var Bus = new Class('Bus',[],[],[],false,'','https://schema.org/BusOrCoach') 439 | Bus.__semanticReference.should.equal('https://schema.org/BusOrCoach') 440 | Bus.getSemanticReference().should.equal('https://schema.org/BusOrCoach') 441 | done() 442 | }) 443 | 444 | it('set the semantic reference (URI) by setSemanticReference function and retrives it', done => { 445 | var Bus = new Class('Bus') 446 | Bus.getSemanticReference.should.be.empty 447 | Bus.setSemanticReference('https://schema.org/BusOrCoach') 448 | Bus.getSemanticReference().should.equal('https://schema.org/BusOrCoach') 449 | done() 450 | }) 451 | }) 452 | }) 453 | -------------------------------------------------------------------------------- /src/Class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * ©2015-2025 Luxembourg Institute of Science and Technology All Rights Reserved 4 | * JavaScript Modelling Framework (JSMF) 5 | * 6 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 8 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 9 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 10 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 11 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 12 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 13 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 14 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 15 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 16 | * POSSIBILITY OF SUCH DAMAGE. 17 | * 18 | * @author J.S. Sottet 19 | * @author N. Biri 20 | * @author A. Vagner 21 | */ 22 | 23 | 'use strict' 24 | 25 | const _ = require('lodash') 26 | , Type = require('./Type') 27 | , Cardinality = require('./Cardinality').Cardinality 28 | 29 | let conformsTo, generateId 30 | (function () { 31 | const Common = require('./Common') 32 | conformsTo = Common.conformsTo 33 | generateId = Common.generateId 34 | }).call() 35 | 36 | /** 37 | * Creation of a JSMF Class. 38 | * 39 | * The attributes are given in a key value manner: the key is the name of the 40 | * attribute, the valu its type. 41 | * 42 | * The references are also given in a key / value way. The name of the 43 | * references are the keys, the value can has the following attrivutes: 44 | * - type: The target type of the reference 45 | * - cardinality: the cardinality of the reference 46 | * - opposite: the name of the opposite reference 47 | * - oppositeCardinality: the cardinality of the opposite reference 48 | * - associated: the type of the associated data. 49 | * 50 | * @constructor 51 | * @param {string} name - Name of the class 52 | * @param {Class[]} superClasses - The superclasses of the current class 53 | * @param {Object} attributes - the attributes of the class. 54 | * @param {Object} attributes - the references of the class. 55 | * @param {Boolean} flexible - set if the class is flexible (i.e., shutting down error handling) or not. 56 | * @param {string} semanticReference - the uri corresponding the ontological element that the class is refering to. 57 | @param {string} description - description of the class providing more guidance to understand its meaning. 58 | * 59 | * @property {string} __name the name of the class 60 | * @property {Class[]} superClasses - the superclasses of this JSMF class 61 | * @property {Object[]} attributes - the attributes of the class 62 | * @property {Object[]} references - the references of the class 63 | * @returns {Class~ClassInstance} 64 | */ 65 | function Class(name, superClasses, attributes, references, flexible, description, semanticReference) { 66 | this.semanticReference = semanticReference 67 | this.description = description 68 | /** The generic class instances Class 69 | * @constructor 70 | * @param {Object} attr The initial values of the instance 71 | */ 72 | function ClassInstance(attr) { 73 | Object.defineProperties(this, 74 | { __jsmf__: {value: elementMeta(ClassInstance)} 75 | }) 76 | createAttributes(this, ClassInstance) 77 | createReferences(this, ClassInstance) 78 | _.forEach(attr, (v,k) => {this[k] = v}) 79 | return new Proxy(this , assignationHandler) 80 | } 81 | 82 | Object.defineProperties(ClassInstance.prototype, { 83 | conformsTo: {value: function () { return conformsTo(this) }, enumerable: false}, 84 | getAssociated : {value: getAssociated, enumerable: false} 85 | }) 86 | superClasses = superClasses || [] 87 | superClasses = _.isArray(superClasses) ? superClasses : [superClasses] 88 | Object.assign(ClassInstance, {__name: name, superClasses, attributes: {}, references: {}, __description : description, __semanticReference : semanticReference}) 89 | ClassInstance.errorCallback = flexible 90 | ? onError.silent 91 | : onError.throw 92 | Object.defineProperty(ClassInstance, '__jsmf__', {value: classMeta()}) 93 | populateClassFunction(ClassInstance) 94 | if (attributes !== undefined) { ClassInstance.addAttributes(attributes)} 95 | if (references !== undefined) { ClassInstance.addReferences(references)} 96 | return ClassInstance 97 | } 98 | 99 | Class.__name = 'Class' 100 | Class.getInheritanceChain = () => [Class] 101 | 102 | Class.newInstance = (name, superClasses, attributes, references) => new Class(name, superClasses, attributes, references) 103 | 104 | /** Return true if the given object is a JSMF Class. 105 | */ 106 | const isJSMFClass = o => conformsTo(o) === Class 107 | 108 | 109 | 110 | function classMeta() { 111 | return {uuid: generateId(), conformsTo: Class} 112 | } 113 | 114 | /** 115 | * Returns the InheritanceChain of this class 116 | * @method 117 | * @memberof Class~ClassInstance 118 | */ 119 | function getInheritanceChain() { 120 | return _(this.superClasses) 121 | .reverse() 122 | .reduce((acc, v) => v.getInheritanceChain().concat(acc), [this]) 123 | } 124 | 125 | /** 126 | * Returns the own and inherited references of this class 127 | * @method 128 | * @memberof Class~ClassInstance 129 | */ 130 | function getAllReferences() { 131 | return _.reduce( 132 | this.getInheritanceChain(), 133 | (acc, cls) => _.reduce(cls.references, (acc2, v, k) => {acc2[k] = v; return acc2}, acc), 134 | {}) 135 | } 136 | 137 | /** 138 | * Returns the own and inherited attributes of this class 139 | * @method 140 | * @memberof Class~ClassInstance 141 | */ 142 | function getAllAttributes() { 143 | return _.reduce( 144 | this.getInheritanceChain(), 145 | (acc, cls) => _.reduce(cls.attributes, (acc2, v, k) => {acc2[k] = v; return acc2}, acc), 146 | {}) 147 | } 148 | 149 | 150 | /** 151 | * Returns the semantic reference of a given class 152 | * 153 | */ 154 | function getSemanticReference() { 155 | return this.__semanticReference 156 | } 157 | 158 | /** 159 | * Returns the description of a given class 160 | * 161 | */ 162 | function getDescription() { 163 | return this.__description 164 | } 165 | 166 | function setSemanticReference(semanticReference) { 167 | this.__semanticReference = semanticReference 168 | } 169 | 170 | 171 | function setDescription(description) { 172 | this.__description = description 173 | } 174 | 175 | function isFlexible() { 176 | return this.errorCallback==onError.silent 177 | } 178 | /** 179 | * Returns the associated data of a reference or of all the references of an object 180 | * @method 181 | * @memberof Class~ClassInstance 182 | * @param {string} name - The name of the reference to explore if undefined, all the references are returned 183 | */ 184 | function getAssociated(name) { 185 | const path = ['__jsmf__', 'associated'] 186 | if (name !== undefined) { 187 | path.push(name) 188 | } 189 | return _.get(this, path) 190 | } 191 | 192 | /** 193 | * Add several references to the Class 194 | * @method 195 | * @memberof Class~ClassInstance 196 | * @param {Object} descriptor - The definition of the attributes, 197 | * the keys are the names of the attribute to create. 198 | * The values contains the description of the attribute. 199 | * See {@link Class~ClassInstance#addAttribute} parameters name 200 | * for the supported property name. 201 | */ 202 | function addReferences(descriptor) { 203 | _.forEach(descriptor, (desc, k) => 204 | this.addReference( 205 | k, 206 | desc.target || desc.type, 207 | desc.cardinality, 208 | desc.opposite, 209 | desc.oppositeCardinality, 210 | desc.associated, 211 | desc.errorCallback, 212 | desc.oppositeErrorCallback) 213 | ) 214 | } 215 | 216 | /** 217 | * Add a reference to the Class 218 | * @method 219 | * @memberof Class~ClassInstance 220 | * @param {string} name - The reference name 221 | * @param {Class} target - The target class. Note that {@link Class} and {@link Model} 222 | * can be targeted as well, even if they are not formally 223 | * instances of {@link Class}. 224 | * @param {Cardinality} sourceCardinality - The cardinality of the reference 225 | * @param {string} opposite - The name of the ooposite reference if any, 226 | * it can be an existing or a new reference name. 227 | * @param {Cardinality} oppositeCardinality - The cardinality of the opposite reference, 228 | * not used if opposite is not set. 229 | * @param {Class} associated - The type of the associated data linked to this reference 230 | * @param {Function} errorCallback - Defines what to do when wrong types are assigned 231 | * @param {Function} oppositeErrorCallback - Defines what to do when wrong types are 232 | * assigned to the opposite reference 233 | */ 234 | function addReference(name, target, sourceCardinality, opposite, oppositeCardinality, associated, errorCallback, oppositeErrorCallback) { 235 | this.references[name] = { type: target || Type.JSMFAny 236 | , cardinality: Cardinality.check(sourceCardinality) 237 | } 238 | if (opposite !== undefined) { 239 | this.references[name].opposite = opposite 240 | target.references[opposite] = 241 | { type: this 242 | , cardinality: oppositeCardinality === undefined && target.references[opposite] !== undefined ? 243 | target.references[opposite].cardinality : 244 | Cardinality.check(oppositeCardinality) 245 | , opposite: name 246 | , errorCallback: oppositeErrorCallback === undefined && target.references[opposite] !== undefined 247 | ? target.references[opposite].oppositeErrorCallback 248 | : this.errorCallback 249 | } 250 | } 251 | if (associated !== undefined) { 252 | this.references[name].associated = associated 253 | if (opposite !== undefined) { 254 | target.references[opposite].associated = associated 255 | } 256 | } 257 | this.references[name].errorCallback = errorCallback || this.errorCallback 258 | } 259 | 260 | /** 261 | * Remove a reference from a class 262 | * @method 263 | * @memberof Class~ClassInstance 264 | * @param {string} name - The name of the reference to remove 265 | * @param {boolean} opposite - true if the opposite reference should be removed as well 266 | */ 267 | function removeReference(name, opposite) { 268 | const ref = this.references[name] 269 | _.unset(this.references, name) 270 | if (ref.opposite !== undefined) { 271 | if (opposite) { 272 | _.unset(ref.type.references, ref.opposite) 273 | } else { 274 | _.unset(ref.type.references[ref.opposite], 'opposite') 275 | } 276 | } 277 | } 278 | 279 | function populateClassFunction(cls) { 280 | Object.defineProperties(cls, 281 | { getInheritanceChain: {value: getInheritanceChain} 282 | , newInstance: {value: init => new cls(init)} 283 | , conformsTo: {value: () => conformsTo(cls)} 284 | , getAllReferences: {value: getAllReferences} 285 | , addReference: {value: addReference} 286 | , removeReference: {value: removeReference} 287 | , setReference: {value: addReference} 288 | , addReferences: {value: addReferences} 289 | , setReferences: {value: addReferences} 290 | , getAllAttributes: {value: getAllAttributes} 291 | , addAttribute: {value: addAttribute} 292 | , removeAttribute: {value: removeAttribute} 293 | , setAttribute: {value: addAttribute} 294 | , addAttributes: {value: addAttributes} 295 | , setAttributes: {value: addAttributes} 296 | , setSuperType: {value: setSuperType} 297 | , setSuperClass: {value: setSuperType} 298 | , setSuperClasses: {value: setSuperType} 299 | , setFlexible: {value: setFlexible} 300 | , isFlexible : {value: isFlexible} 301 | , setSemanticReference : {value : setSemanticReference} 302 | , getSemanticReference : {value : getSemanticReference} 303 | , setDescription : {value : setDescription} 304 | , getDescription : {value : getDescription} 305 | }) 306 | } 307 | 308 | /** Change superClasses of this class. 309 | * 310 | * @method 311 | * @memberof Class~ClassInstance 312 | * @param s - Either a {@link Class} or an array of {@link Class} 313 | */ 314 | function setSuperType(s) { 315 | const ss = _.isArray(s) ? s : [s] 316 | this.superClasses = _.uniq(this.superClasses.concat(ss)) 317 | } 318 | 319 | /** Add an attribute to a class. 320 | * @method 321 | * @memberof Class~ClassInstance 322 | * @param {string} name - The name of the attribute 323 | * @param {Function} type - In jsmf an attribute Type is a function that returns true if the 324 | * value is a member of this type, false otherwise. Some predefined 325 | * types are available in the class {@link Type}. 326 | * Users can also use builtin JavaScript types, that are replaced 327 | * on the fly by the corresponding validation function 328 | * @param {boolean} mandatory - If set to true, the attribute can't be set to undefined. 329 | * @param {Function} errorCallback - defines what to do if an invalid value is set to this attribute. 330 | */ 331 | function addAttribute(name, type, mandatory, errorCallback) { 332 | this.attributes[name] = 333 | { type: Type.normalizeType(type) 334 | , mandatory: mandatory || false 335 | , errorCallback: errorCallback || this.errorCallback 336 | } 337 | } 338 | 339 | /** Remove an attribute to a class 340 | * @method 341 | * @memberof Class~ClassInstance 342 | * @param {string} name - The name of the attribute 343 | */ 344 | function removeAttribute(name) { 345 | _.unset(this.attributes, name) 346 | } 347 | 348 | /** Add several attributes 349 | * @method 350 | * @memberof Class~ClassInstance 351 | * @param {Object} attrs - The attributes to add. The keys are te attribute name, 352 | * the values are the attributes descriptors. 353 | * See {@link Class~ClassInstance#RemoveAttribute} for 354 | * the supported properties. 355 | */ 356 | function addAttributes(attrs) { 357 | _.forEach(attrs, (v, k) => { 358 | if (v.type !== undefined) { 359 | this.addAttribute(k, v.type, v.mandatory, v.errorCallback) 360 | } else { 361 | this.addAttribute(k, v) 362 | } 363 | }) 364 | } 365 | 366 | function createAttributes(e, cls) { 367 | _.forEach(cls.getAllAttributes(), (desc, name) => { 368 | e.__jsmf__.attributes[name] = undefined 369 | createAttribute(e, name, desc) 370 | }) 371 | } 372 | 373 | function createReferences(e, cls) { 374 | _.forEach(cls.getAllReferences(), (desc, name) => { 375 | e.__jsmf__.associated[name] = [] 376 | e.__jsmf__.references[name] = [] 377 | createAddReference(e, name, desc) 378 | createRemoveReference(e, name) 379 | createReference(e, name, desc) 380 | }) 381 | } 382 | 383 | function createAttribute(o, name, desc) { 384 | createSetAttribute(o,name, desc) 385 | Object.defineProperty(o, name, 386 | { get: () => o.__jsmf__.attributes[name] 387 | , set: o[setName(name)] 388 | , enumerable: true 389 | , configurable: true 390 | } 391 | ) 392 | } 393 | 394 | function createSetAttribute(o, name, desc) { 395 | Object.defineProperty(o, setName(name), 396 | { value: x => { 397 | if (!desc.type(x) && (desc.mandatory || !_.isUndefined(x))) { 398 | desc.errorCallback(o, name, x) 399 | } 400 | o.__jsmf__.attributes[name] = x 401 | } 402 | , configurable: true 403 | }) 404 | } 405 | 406 | /** Check whether a class (or some classes) are in the inheritance chain of a given class 407 | * @function 408 | * @param {Class} x - The class to test 409 | * @param type - Either a {@link Class} or an array of classes tha should be in the class x. 410 | */ 411 | function hasClass(x, type) { 412 | const types = _.isArray(type) ? type : [type] 413 | return _.some(x.conformsTo().getInheritanceChain(), c => _.includes(types, c)) 414 | } 415 | 416 | 417 | function createReference(o, name, desc) { 418 | Object.defineProperty(o, name, 419 | { get: () => o.__jsmf__.references[name] 420 | , set: xs => { 421 | xs = _.isArray(xs) ? xs : [xs] 422 | const invalid = _.filter(xs, x => { 423 | const type = conformsTo(x) 424 | return type === undefined 425 | || !(_.includes(type.getInheritanceChain(), desc.type) || (desc.type === Type.JSMFAny)) 426 | }) 427 | if (!_.isEmpty(invalid)) { 428 | desc.errorCallback(o, name, xs) 429 | } 430 | o.__jsmf__.associated[name] = [] 431 | if (desc.opposite !== undefined) { 432 | const removed = _.difference(o[name], xs) 433 | _.forEach(removed, function(y) { 434 | _.remove(y.__jsmf__.references[desc.opposite], z => z === o) 435 | }) 436 | const added = _.difference(xs, o[name]) 437 | _.forEach(added, y => y.__jsmf__.references[desc.opposite].push(o)) 438 | } 439 | o.__jsmf__.references[name] = xs 440 | } 441 | , enumerable: true 442 | , configurable: true 443 | }) 444 | } 445 | 446 | 447 | function createAddReference(o, name, desc) { 448 | Object.defineProperty(o, addName(name), 449 | { value: function(xs, associated) { 450 | xs = _.isArray(xs) ? xs : [xs] 451 | const associationMap = o.__jsmf__.associated 452 | const backup = associationMap[name] 453 | o.__jsmf__.references[name] = o[name].concat(xs) 454 | if (desc.opposite !== undefined) { 455 | _.forEach(xs, y => y.__jsmf__.references[desc.opposite].push(o)) 456 | } 457 | associationMap[name] = backup 458 | if (associated !== undefined) { 459 | associationMap[name] = associationMap[name] || [] 460 | if (desc.associated !== undefined 461 | && !_.includes(associated.conformsTo().getInheritanceChain(), desc.associated)) { 462 | throw new Error(`Invalid association ${associated} for object ${o}`) 463 | } 464 | _.forEach(xs, x => { 465 | associationMap[name].push({elem: x, associated}) 466 | if (desc.opposite !== undefined) { 467 | x.__jsmf__.associated[desc.opposite].push({elem: o, associated}) 468 | } 469 | }) 470 | } 471 | } 472 | , configurable: true 473 | }) 474 | } 475 | 476 | function createRemoveReference(o, name) { 477 | Object.defineProperty(o, removeName(name), 478 | { value: xs => { 479 | xs = _.isArray(xs) ? xs : [xs] 480 | const associationMap = o.__jsmf__.associated 481 | associationMap[name] = _.differenceWith(associationMap[name], xs, (x,y) => x.elem === y) 482 | o.__jsmf__.references[name] = _.difference(o.__jsmf__.references[name], xs) 483 | } 484 | , configurable: true 485 | }) 486 | } 487 | 488 | 489 | 490 | 491 | /** Decide whether whether or not type will be check for attributes and 492 | * references of a whole class. 493 | * @method 494 | * @memberof Class~ClassInstance 495 | * @param {boolean} b - If true, type is not checked on assignement, 496 | * if false wrong assignement type riase an error. 497 | */ 498 | function setFlexible(b) { 499 | this.errorCallback = b ? onError.silent : onError.throw 500 | _.forEach(this.references, r => r.errorCallback = this.errorCallback) 501 | _.forEach(this.attributes, r => r.errorCallback = this.errorCallback) 502 | } 503 | 504 | function elementMeta(constructor) { 505 | return { conformsTo: constructor 506 | , uuid: generateId() 507 | , attributes: {} 508 | , references: {} 509 | , associated: {} 510 | } 511 | } 512 | 513 | 514 | function addName(n) { 515 | return prefixedName('add',n) 516 | } 517 | 518 | function setName(n) { 519 | return prefixedName('set',n) 520 | } 521 | 522 | function removeName(n) { 523 | return prefixedName('remove',n) 524 | } 525 | 526 | function prefixedName(pre, n) { 527 | return pre + _.upperFirst(n) 528 | } 529 | 530 | function refreshElement(o) { 531 | const mm = o.conformsTo() 532 | if (!mm) {return o} 533 | const oBackup = Object.assign({}, o) 534 | for (let x in o) {delete o[x]} 535 | createAttributes(o, mm) 536 | createReferences(o, mm) 537 | _.forEach(oBackup, (v, k) => { 538 | if (v !== undefined) { o[k] = v } 539 | }) 540 | return o 541 | } 542 | 543 | /** Predefined function for assignation error handling 544 | * It is used in initialisation of classInstance 545 | */ 546 | const assignationHandler = { 547 | set(target, property, value) { 548 | const flex = target.conformsTo().errorCallback==onError.silent 549 | if (!(property in target) && !flex ) { 550 | throw new Error(`Cannot add property ${property}` ); 551 | } 552 | target[property] = value; 553 | return true; 554 | } 555 | }; 556 | 557 | /** Predefined function for type error handling. 558 | * It can be used in {@link Class~ClassInstance#addReference} and {@link Class~ClassInstance#addAttribute} 559 | */ 560 | const onError = 561 | { /** Raise an error on type error 562 | * @member 563 | */ 564 | 'throw': function(o,n,x) {throw new TypeError(`Invalid assignment: ${x} for property ${n} of object ${o}`)} 565 | , /** Assign and log the error on type error 566 | * @member 567 | */ 568 | 'log': function(o,n,x) {console.log(`assignment: ${x} for property ${n} of object ${o}`)} 569 | , /** Assign anyway. 570 | * @member 571 | */ 572 | 'silent': function() {} 573 | } 574 | 575 | module.exports = { Class, isJSMFClass, hasClass, onError, refreshElement } 576 | --------------------------------------------------------------------------------