diff --git a/platform/core/bundle.js b/platform/core/bundle.js index 9449a90454..259eee6fd9 100644 --- a/platform/core/bundle.js +++ b/platform/core/bundle.js @@ -24,7 +24,6 @@ define([ "./src/objects/DomainObjectProvider", "./src/capabilities/CoreCapabilityProvider", "./src/models/StaticModelProvider", - "./src/models/RootModelProvider", "./src/models/ModelAggregator", "./src/models/ModelCacheService", "./src/models/PersistedModelProvider", @@ -57,7 +56,6 @@ define([ DomainObjectProvider, CoreCapabilityProvider, StaticModelProvider, - RootModelProvider, ModelAggregator, ModelCacheService, PersistedModelProvider, @@ -152,16 +150,6 @@ define([ "$log" ] }, - { - "provides": "modelService", - "type": "provider", - "implementation": RootModelProvider, - "depends": [ - "roots[]", - "$q", - "$log" - ] - }, { "provides": "modelService", "type": "aggregator", diff --git a/platform/core/src/models/RootModelProvider.js b/platform/core/src/models/RootModelProvider.js deleted file mode 100644 index 205d41e66b..0000000000 --- a/platform/core/src/models/RootModelProvider.js +++ /dev/null @@ -1,79 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * Module defining RootModelProvider. Created by vwoeltje on 11/7/14. - */ -define( - ['./StaticModelProvider'], - function (StaticModelProvider) { - - /** - * Provides the root object (id = "ROOT"), which is the top-level - * domain object shown when the application is started, from which all - * other domain objects are reached. - * - * The root model provider works as the static model provider, - * except that it aggregates roots[] instead of models[], and - * exposes them all as composition of the root object ROOT, - * whose model is also provided by this service. - * - * @memberof platform/core - * @constructor - * @implements {ModelService} - * @param {Array} roots all `roots[]` extensions - * @param $q Angular's $q, for promises - * @param $log Angular's $log, for logging - */ - function RootModelProvider(roots, $q, $log) { - // Pull out identifiers to used as ROOT's - var ids = roots.map(function (root) { - return root.id; - }); - - // Assign an initial location to root models - roots.forEach(function (root) { - if (!root.model) { - root.model = {}; - } - root.model.location = 'ROOT'; - }); - - this.baseProvider = new StaticModelProvider(roots, $q, $log); - this.rootModel = { - name: "The root object", - type: "root", - composition: ids - }; - } - - RootModelProvider.prototype.getModels = function (ids) { - var rootModel = this.rootModel; - return this.baseProvider.getModels(ids).then(function (models) { - models.ROOT = rootModel; - return models; - }); - }; - - return RootModelProvider; - } -); diff --git a/platform/core/test/models/RootModelProviderSpec.js b/platform/core/test/models/RootModelProviderSpec.js deleted file mode 100644 index a7e027c4ab..0000000000 --- a/platform/core/test/models/RootModelProviderSpec.js +++ /dev/null @@ -1,105 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * RootModelProviderSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../../src/models/RootModelProvider"], - function (RootModelProvider) { - - describe("The root model provider", function () { - var roots = [ - { - "id": "a", - "model": { - "name": "Thing A", - "someProperty": "Some Value A" - } - }, - { - "id": "b", - "model": { - "name": "Thing B", - "someProperty": "Some Value B" - } - } - ], - captured, - mockLog, - mockQ, - provider; - - function mockPromise(value) { - return { - then: function (callback) { - return mockPromise(callback(value)); - } - }; - } - - function capture(value) { - captured = value; - } - - - beforeEach(function () { - mockQ = { when: mockPromise }; - mockLog = jasmine.createSpyObj("$log", ["error", "warn", "info", "debug"]); - provider = new RootModelProvider(roots, mockQ, mockLog); - }); - - it("provides models from extension declarations", function () { - // Verify that we got the promise as the return value - provider.getModels(["a", "b"]).then(capture); - - // Verify that the promise has the desired models - expect(captured.a.name).toEqual("Thing A"); - expect(captured.a.someProperty).toEqual("Some Value A"); - expect(captured.b.name).toEqual("Thing B"); - expect(captured.b.someProperty).toEqual("Some Value B"); - }); - - it("provides models with a location", function () { - provider.getModels(["a", "b"]).then(capture); - expect(captured.a.location).toBe('ROOT'); - expect(captured.b.location).toBe('ROOT'); - }); - - - it("does not provide models which are not in extension declarations", function () { - provider.getModels(["c"]).then(capture); - - // Verify that the promise has the desired models - expect(captured.c).toBeUndefined(); - }); - - it("provides a ROOT object with roots in its composition", function () { - provider.getModels(["ROOT"]).then(capture); - - expect(captured.ROOT).toBeDefined(); - expect(captured.ROOT.composition).toEqual(["a", "b"]); - }); - - }); - } -); diff --git a/src/MCT.js b/src/MCT.js index 31cf0dbf47..3bfe5417a1 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -61,9 +61,11 @@ define([ services: [ { key: "openmct", - implementation: function () { + implementation: function ($injector) { + this.$injector = $injector; return this; - }.bind(this) + }.bind(this), + depends: ['$injector'] } ] } }; @@ -96,7 +98,7 @@ define([ * @memberof module:openmct.MCT# * @name composition */ - this.composition = new api.CompositionAPI(); + this.composition = new api.CompositionAPI(this); /** * Registry for views of domain objects which should appear in the diff --git a/src/adapter/capabilities/AlternateCompositionCapability.js b/src/adapter/capabilities/AlternateCompositionCapability.js index 20cceb9d10..f85ef2d57c 100644 --- a/src/adapter/capabilities/AlternateCompositionCapability.js +++ b/src/adapter/capabilities/AlternateCompositionCapability.js @@ -71,7 +71,7 @@ define([ this.getDependencies(); } - var keyString = objectUtils.makeKeyString(child.key); + var keyString = objectUtils.makeKeyString(child.identifier); var oldModel = objectUtils.toOldFormat(child); var newDO = this.instantiate(oldModel, keyString); return this.contextualize(newDO, this.domainObject); @@ -89,9 +89,9 @@ define([ } var collection = this.openmct.composition.get(newFormatDO); + return collection.load() .then(function (children) { - collection.destroy(); return children.map(this.contextualizeChild, this); }.bind(this)); }; diff --git a/src/adapter/runs/AlternateCompositionInitializer.js b/src/adapter/runs/AlternateCompositionInitializer.js index c9169ab55b..2129086b8c 100644 --- a/src/adapter/runs/AlternateCompositionInitializer.js +++ b/src/adapter/runs/AlternateCompositionInitializer.js @@ -28,7 +28,7 @@ define([ // cannot be injected. function AlternateCompositionInitializer(openmct) { AlternateCompositionCapability.appliesTo = function (model) { - return !model.composition && !!openmct.composition.get(model); + return !!openmct.composition.get(model); }; } diff --git a/src/api/composition/CompositionAPI.js b/src/api/composition/CompositionAPI.js index 31073bd956..1f65c68447 100644 --- a/src/api/composition/CompositionAPI.js +++ b/src/api/composition/CompositionAPI.js @@ -41,10 +41,11 @@ define([ * @returns {module:openmct.CompositionCollection} * @memberof module:openmct */ - function CompositionAPI() { + function CompositionAPI(publicAPI) { this.registry = []; this.policies = []; - this.addProvider(new DefaultCompositionProvider()); + this.addProvider(new DefaultCompositionProvider(publicAPI)); + this.publicAPI = publicAPI; } /** @@ -77,7 +78,7 @@ define([ return; } - return new CompositionCollection(domainObject, provider); + return new CompositionCollection(domainObject, provider, this.publicAPI); }; /** diff --git a/src/api/composition/CompositionCollection.js b/src/api/composition/CompositionCollection.js index 1d0d0669d5..c9bff422c9 100644 --- a/src/api/composition/CompositionCollection.js +++ b/src/api/composition/CompositionCollection.js @@ -21,20 +21,26 @@ *****************************************************************************/ define([ - 'EventEmitter', 'lodash', '../objects/object-utils' ], function ( - EventEmitter, _, objectUtils ) { - /** * A CompositionCollection represents the list of domain objects contained * by another domain object. It provides methods for loading this - * list asynchronously, and for modifying this list. + * list asynchronously, modifying this list, and listening for changes to + * this list. + * + * Usage: + * ```javascript + * var myViewComposition = MCT.composition.get(myViewObject); + * myViewComposition.on('add', addObjectToView); + * myViewComposition.on('remove', removeObjectFromView); + * myViewComposition.load(); // will trigger `add` for all loaded objects. + * ``` * * @interface CompositionCollection * @param {module:openmct.DomainObject} domainObject the domain object @@ -44,20 +50,42 @@ define([ * @param {module:openmct.CompositionAPI} api the composition API, for * policy checks * @memberof module:openmct - * @augments EventEmitter */ - function CompositionCollection(domainObject, provider, api) { - EventEmitter.call(this); + function CompositionCollection(domainObject, provider, publicAPI) { this.domainObject = domainObject; this.provider = provider; - this.api = api; - if (this.provider.on) { + this.publicAPI = publicAPI; + this.listeners = { + add: [], + remove: [], + load: [] + }; + this.onProviderAdd = this.onProviderAdd.bind(this); + this.onProviderRemove = this.onProviderRemove.bind(this); + } + + + /** + * Listen for changes to this composition. Supports 'add', 'remove', and + * 'load' events. + * + * @param event event to listen for, either 'add', 'remove' or 'load'. + * @param callback to trigger when event occurs. + * @param [context] context to use when invoking callback, optional. + */ + CompositionCollection.prototype.on = function (event, callback, context) { + if (!this.listeners[event]) { + throw new Error('Event not supported by composition: ' + event); + } + + if (event === 'add') { this.provider.on( this.domainObject, 'add', this.onProviderAdd, this ); + } if (event === 'remove') { this.provider.on( this.domainObject, 'remove', @@ -65,62 +93,55 @@ define([ this ); } - } - CompositionCollection.prototype = Object.create(EventEmitter.prototype); - - CompositionCollection.prototype.onProviderAdd = function (child) { - this.add(child, true); - }; - - CompositionCollection.prototype.onProviderRemove = function (child) { - this.remove(child, true); - }; - - /** - * Get the index of a domain object within this composition. If the - * domain object is not contained here, -1 will be returned. - * - * A call to [load]{@link module:openmct.CompositionCollection#load} - * must have resolved before using this method. - * - * @param {module:openmct.DomainObject} child the domain object for which - * an index should be retrieved - * @returns {number} the index of that domain object - * @memberof module:openmct.CompositionCollection# - * @name indexOf - */ - CompositionCollection.prototype.indexOf = function (child) { - return _.findIndex(this.loadedChildren, function (other) { - return objectUtils.equals(child, other); + this.listeners[event].push({ + callback: callback, + context: context }); }; /** - * Get the index of a domain object within this composition. + * Remove a listener. Must be called with same exact parameters as + * `off`. * - * A call to [load]{@link module:openmct.CompositionCollection#load} - * must have resolved before using this method. - * - * @param {module:openmct.DomainObject} child the domain object for which - * containment should be checked - * @returns {boolean} true if the domain object is contained here - * @memberof module:openmct.CompositionCollection# - * @name contains + * @param event + * @param callback + * @param [context] */ - CompositionCollection.prototype.contains = function (child) { - return this.indexOf(child) !== -1; - }; - /** - * Check if a domain object can be added to this composition. - * - * @param {module:openmct.DomainObject} child the domain object to add - * @memberof module:openmct.CompositionCollection# - * @name canContain - */ - CompositionCollection.prototype.canContain = function (domainObject) { - return this.api.checkPolicy(this.domainObject, domainObject); + CompositionCollection.prototype.off = function (event, callback, context) { + if (!this.listeners[event]) { + throw new Error('Event not supported by composition: ' + event); + } + + var index = _.findIndex(this.listeners[event], function (l) { + return l.callback === callback && l.context === context; + }); + + if (index === -1) { + throw new Error('Tried to remove a listener that does not exist'); + } + + this.listeners[event].splice(index, 1); + if (this.listeners[event].length === 0) { + // Remove provider listener if this is the last callback to + // be removed. + if (event === 'add') { + this.provider.off( + this.domainObject, + 'add', + this.onProviderAdd, + this + ); + } else if (event === 'remove') { + this.provider.off( + this.domainObject, + 'remove', + this.onProviderRemove, + this + ); + } + } }; /** @@ -136,23 +157,10 @@ define([ * @name add */ CompositionCollection.prototype.add = function (child, skipMutate) { - if (!this.loadedChildren) { - throw new Error("Must load composition before you can add!"); - } - if (!this.canContain(child)) { - throw new Error("This object cannot contain that object."); - } - if (this.contains(child)) { - if (skipMutate) { - return; // don't add twice, don't error. - } - throw new Error("Unable to add child: already in composition"); - } - this.loadedChildren.push(child); - this.emit('add', child); if (!skipMutate) { - // add after we have added. - this.provider.add(this.domainObject, child); + this.provider.add(this.domainObject, child.identifier); + } else { + this.emit('add', child); } }; @@ -167,12 +175,11 @@ define([ CompositionCollection.prototype.load = function () { return this.provider.load(this.domainObject) .then(function (children) { - this.loadedChildren = []; - children.map(function (c) { - this.add(c, true); - }, this); + return Promise.all(children.map(this.onProviderAdd, this)); + }.bind(this)) + .then(function (children) { this.emit('load'); - return this.loadedChildren.slice(); + return children; }.bind(this)); }; @@ -189,42 +196,44 @@ define([ * @name remove */ CompositionCollection.prototype.remove = function (child, skipMutate) { - if (!this.contains(child)) { - if (skipMutate) { - return; - } - throw new Error("Unable to remove child: not found in composition"); - } - var index = this.indexOf(child); - var removed = this.loadedChildren.splice(index, 1)[0]; - this.emit('remove', index, child); if (!skipMutate) { - // trigger removal after we have internally removed it. - this.provider.remove(this.domainObject, removed); + this.provider.remove(this.domainObject, child.identifier); + } else { + this.emit('remove', child); } }; /** - * Stop using this composition collection. This will release any resources - * associated with this collection. - * @name destroy - * @memberof module:openmct.CompositionCollection# + * Handle adds from provider. + * @private */ - CompositionCollection.prototype.destroy = function () { - if (this.provider.off) { - this.provider.off( - this.domainObject, - 'add', - this.onProviderAdd, - this - ); - this.provider.off( - this.domainObject, - 'remove', - this.onProviderRemove, - this - ); - } + CompositionCollection.prototype.onProviderAdd = function (childId) { + return this.publicAPI.objects.get(childId).then(function (child) { + this.add(child, true); + return child; + }.bind(this)); + }; + + /** + * Handle removal from provider. + * @private + */ + CompositionCollection.prototype.onProviderRemove = function (child) { + this.remove(child, true); + }; + + /** + * Emit events. + * @private + */ + CompositionCollection.prototype.emit = function (event, payload) { + this.listeners[event].forEach(function (l) { + if (l.context) { + l.callback.call(l.context, payload); + } else { + l.callback(payload); + } + }); }; return CompositionCollection; diff --git a/src/api/composition/DefaultCompositionProvider.js b/src/api/composition/DefaultCompositionProvider.js index 28efd33a2c..fe92530a1e 100644 --- a/src/api/composition/DefaultCompositionProvider.js +++ b/src/api/composition/DefaultCompositionProvider.js @@ -22,35 +22,32 @@ define([ 'lodash', - 'EventEmitter', - '../objects/ObjectAPI', '../objects/object-utils' ], function ( _, - EventEmitter, - ObjectAPI, objectUtils ) { /** * A CompositionProvider provides the underlying implementation of * composition-related behavior for certain types of domain object. * + * By default, a composition provider will not support composition + * modification. You can add support for mutation of composition by + * defining `add` and/or `remove` methods. + * + * If the composition of an object can change over time-- perhaps via + * server updates or mutation via the add/remove methods, then one must + * trigger events as necessary. + * * @interface CompositionProvider * @memberof module:openmct - * @augments EventEmitter */ - function makeEventName(domainObject, event) { - return event + ':' + objectUtils.makeKeyString(domainObject.identifier); + function DefaultCompositionProvider(publicAPI) { + this.publicAPI = publicAPI; + this.listeningTo = {}; } - function DefaultCompositionProvider() { - EventEmitter.call(this); - } - - DefaultCompositionProvider.prototype = - Object.create(EventEmitter.prototype); - /** * Check if this provider should be used to load composition for a * particular domain object. @@ -68,45 +65,78 @@ define([ /** * Load any domain objects contained in the composition of this domain * object. - * @param {module:openmct.DomainObjcet} domainObject the domain object + * @param {module:openmct.DomainObject} domainObject the domain object * for which to load composition - * @returns {Promise.>} a promise for - * the domain objects in this composition + * @returns {Promise.>} a promise for + * the Identifiers in this composition * @memberof module:openmct.CompositionProvider# * @method load */ DefaultCompositionProvider.prototype.load = function (domainObject) { - return Promise.all(domainObject.composition.map(ObjectAPI.get)); + return Promise.all(domainObject.composition); }; + /** + * Attach listeners for changes to the composition of a given domain object. + * Supports `add` and `remove` events. + * + * @param {module:openmct.DomainObject} domainObject to listen to + * @param String event the event to bind to, either `add` or `remove`. + * @param Function callback callback to invoke when event is triggered. + * @param [context] context to use when invoking callback. + */ DefaultCompositionProvider.prototype.on = function ( domainObject, event, - listener, + callback, context ) { - // these can likely be passed through to the mutation service instead - // of using an eventemitter. - this.addListener( - makeEventName(domainObject, event), - listener, - context - ); + this.establishTopicListener(); + + var keyString = objectUtils.makeKeyString(domainObject.identifier); + var objectListeners = this.listeningTo[keyString]; + + if (!objectListeners) { + objectListeners = this.listeningTo[keyString] = { + add: [], + remove: [], + composition: [].slice.apply(domainObject.composition) + }; + } + + objectListeners[event].push({ + callback: callback, + context: context + }); }; + /** + * Remove a listener that was previously added for a given domain object. + * event name, callback, and context must be the same as when the listener + * was originally attached. + * + * @param {module:openmct.DomainObject} domainObject to remove listener for + * @param String event event to stop listening to: `add` or `remove`. + * @param Function callback callback to remove. + * @param [context] context of callback to remove. + */ DefaultCompositionProvider.prototype.off = function ( domainObject, event, - listener, + callback, context ) { - // these can likely be passed through to the mutation service instead - // of using an eventemitter. - this.removeListener( - makeEventName(domainObject, event), - listener, - context - ); + var keyString = objectUtils.makeKeyString(domainObject.identifier); + var objectListeners = this.listeningTo[keyString]; + + var index = _.findIndex(objectListeners[event], function (l) { + return l.callback === callback && l.context === context; + }); + + objectListeners[event].splice(index, 1); + if (!objectListeners.add.length && !objectListeners.remove.length) { + delete this.listeningTo[keyString]; + } }; /** @@ -121,11 +151,9 @@ define([ * @memberof module:openmct.CompositionProvider# * @method remove */ - DefaultCompositionProvider.prototype.remove = function (domainObject, child) { - // TODO: this needs to be synchronized via mutation - var index = domainObject.composition.indexOf(child); - domainObject.composition.splice(index, 1); - this.emit(makeEventName(domainObject, 'remove'), child); + DefaultCompositionProvider.prototype.remove = function (domainObject, childId) { + // TODO: this needs to be synchronized via mutation. + throw new Error('Default Provider does not implement removal.'); }; /** @@ -141,9 +169,64 @@ define([ * @method add */ DefaultCompositionProvider.prototype.add = function (domainObject, child) { + throw new Error('Default Provider does not implement adding.'); // TODO: this needs to be synchronized via mutation - domainObject.composition.push(child.key); - this.emit(makeEventName(domainObject, 'add'), child); + }; + + /** + * Listens on general mutation topic, using injector to fetch to avoid + * circular dependencies. + * + * @private + */ + DefaultCompositionProvider.prototype.establishTopicListener = function () { + if (this.topicListener) { + return; + } + var topic = this.publicAPI.$injector.get('topic'); + var mutation = topic('mutation'); + this.topicListener = mutation.listen(this.onMutation.bind(this)); + }; + + /** + * Handles mutation events. If there are active listeners for the mutated + * object, detects changes to composition and triggers necessary events. + * + * @private + */ + DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) { + var id = oldDomainObject.getId(); + var listeners = this.listeningTo[id]; + + if (!listeners) { + return; + } + + var oldComposition = listeners.composition.map(objectUtils.makeKeyString); + var newComposition = oldDomainObject.getModel().composition; + + var added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString); + var removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString); + + function notify(value) { + return function (listener) { + if (listener.context) { + listener.callback.call(listener.context, value); + } else { + listener.callback(value); + } + }; + } + + added.forEach(function (addedChild) { + listeners.add.forEach(notify(addedChild)); + }); + + removed.forEach(function (removedChild) { + listeners.remove.forEach(notify(removedChild)); + }); + + listeners.composition = newComposition.map(objectUtils.parseKeyString); }; return DefaultCompositionProvider; diff --git a/src/api/objects/RootObjectProvider.js b/src/api/objects/RootObjectProvider.js index 4b44d66f9c..8cabcb95f9 100644 --- a/src/api/objects/RootObjectProvider.js +++ b/src/api/objects/RootObjectProvider.js @@ -32,6 +32,10 @@ define([ return this.rootRegistry.getRoots() .then(function (roots) { return { + identifier: { + key: "ROOT", + namespace: "" + }, name: 'The root object', type: 'root', composition: roots diff --git a/src/api/objects/object-utils.js b/src/api/objects/object-utils.js index 2907b3c6c8..aa8c656252 100644 --- a/src/api/objects/object-utils.js +++ b/src/api/objects/object-utils.js @@ -26,31 +26,50 @@ define([ ) { - // take a key string and turn it into a key object - // 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'} - var parseKeyString = function (identifier) { - if (typeof identifier === 'object') { - return identifier; + /** + * Utility for checking if a thing is an Open MCT Identifier. + * @private + */ + function isIdentifier(thing) { + return typeof thing === 'object' && + thing.hasOwnProperty('key') && + thing.hasOwnProperty('namespace'); + } + + /** + * Utility for checking if a thing is a key string. Not perfect. + * @private + */ + function isKeyString(thing) { + return typeof thing === 'string'; + } + + /** + * Convert a keyString into an Open MCT Identifier, ex: + * 'scratch:root' ==> {namespace: 'scratch', key: 'root'} + * + * Idempotent. + * + * @param keyString + * @returns identifier + */ + function parseKeyString(keyString) { + if (isIdentifier(keyString)) { + return keyString; } var namespace = '', - key = identifier; - for (var i = 0, escaped = false; i < key.length; i++) { - if (escaped) { - escaped = false; - namespace += key[i]; - } else { - if (identifier[i] === "\\") { - escaped = true; - } else if (identifier[i] === ":") { - // namespace = key.slice(0, i); - key = identifier.slice(i + 1); - break; - } - namespace += identifier[i]; + key = keyString; + for (var i = 0; i < key.length; i++) { + if (key[i] === "\\" && key[i + 1] === ":") { + i++; // skip escape character. + } else if (key[i] === ":") { + key = key.slice(i + 1); + break; } + namespace += key[i]; } - if (identifier === namespace) { + if (keyString === namespace) { namespace = ''; } @@ -58,52 +77,95 @@ define([ namespace: namespace, key: key }; - }; + } - // take a key and turn it into a key string - // {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root' - var makeKeyString = function (identifier) { - if (typeof identifier === 'string') { + + /** + * Convert an Open MCT Identifier into a keyString, ex: + * {namespace: 'scratch', key: 'root'} ==> 'scratch:root' + * + * Idempotent + * + * @param identifier + * @returns keyString + */ + function makeKeyString(identifier) { + if (isKeyString(identifier)) { return identifier; } if (!identifier.namespace) { return identifier.key; } return [ - identifier.namespace.replace(':', '\\:'), - identifier.key.replace(':', '\\:') + identifier.namespace.replace(/\:/g, '\\:'), + identifier.key ].join(':'); - }; + } - // Converts composition to use key strings instead of keys - var toOldFormat = function (model) { + /** + * Convert a new domain object into an old format model, removing the + * identifier and converting the composition array from Open MCT Identifiers + * to old format keyStrings. + * + * @param domainObject + * @returns oldFormatModel + */ + function toOldFormat(model) { model = JSON.parse(JSON.stringify(model)); delete model.identifier; if (model.composition) { model.composition = model.composition.map(makeKeyString); } return model; - }; + } - // converts composition to use keys instead of key strings - var toNewFormat = function (model, identifier) { + /** + * Convert an old format domain object model into a new format domain + * object. Adds an identifier using the provided keyString, and converts + * the composition array to utilize Open MCT Identifiers. + * + * @param model + * @param keyString + * @returns domainObject + */ + function toNewFormat(model, keyString) { model = JSON.parse(JSON.stringify(model)); - model.identifier = parseKeyString(identifier); + model.identifier = parseKeyString(keyString); if (model.composition) { model.composition = model.composition.map(parseKeyString); } return model; - }; + } - var equals = function (a, b) { - return makeKeyString(a.key) === makeKeyString(b.key); - }; + /** + * Compare two Open MCT Identifiers, returning true if they are equal. + * + * @param identifier + * @param otherIdentifier + * @returns Boolean true if identifiers are equal. + */ + function identifierEquals(a, b) { + return a.key === b.key && a.namespace === b.namespace; + } + + /** + * Compare two domain objects, return true if they're the same object. + * Equality is determined by identifier. + * + * @param domainObject + * @param otherDomainOBject + * @returns Boolean true if objects are equal. + */ + function objectEquals(a, b) { + return identifierEquals(a.identifier, b.identifier); + } return { toOldFormat: toOldFormat, toNewFormat: toNewFormat, makeKeyString: makeKeyString, parseKeyString: parseKeyString, - equals: equals + equals: objectEquals, + identifierEquals: identifierEquals }; }); diff --git a/src/api/objects/test/RootObjectProviderSpec.js b/src/api/objects/test/RootObjectProviderSpec.js index 2734c2e55f..7691e8226a 100644 --- a/src/api/objects/test/RootObjectProviderSpec.js +++ b/src/api/objects/test/RootObjectProviderSpec.js @@ -48,6 +48,10 @@ define([ rootObjectProvider.get() .then(function (root) { expect(root).toEqual({ + identifier: { + key: "ROOT", + namespace: "" + }, name: 'The root object', type: 'root', composition: ['some root'] diff --git a/src/api/objects/test/object-utilsSpec.js b/src/api/objects/test/object-utilsSpec.js new file mode 100644 index 0000000000..cc3f26ddd9 --- /dev/null +++ b/src/api/objects/test/object-utilsSpec.js @@ -0,0 +1,153 @@ +define([ + '../object-utils' +], function ( + objectUtils +) { + describe('objectUtils', function () { + + + describe('keyString util', function () { + var EXPECTATIONS = { + 'ROOT': { + namespace: '', + key: 'ROOT' + }, + 'mine': { + namespace: '', + key: 'mine' + }, + 'extended:something:with:colons': { + key: 'something:with:colons', + namespace: 'extended' + }, + 'https\\://some/url:resourceId': { + key: 'resourceId', + namespace: 'https://some/url' + }, + 'scratch:root': { + namespace: 'scratch', + key: 'root' + }, + 'thingy\\:thing:abc123': { + namespace: 'thingy:thing', + key: 'abc123' + } + }; + + Object.keys(EXPECTATIONS).forEach(function (keyString) { + it('parses "' + keyString + '".', function () { + expect(objectUtils.parseKeyString(keyString)) + .toEqual(EXPECTATIONS[keyString]); + }); + + it('parses and re-encodes "' + keyString + '"', function () { + var identifier = objectUtils.parseKeyString(keyString); + expect(objectUtils.makeKeyString(identifier)) + .toEqual(keyString); + }); + + it('is idempotent for "' + keyString + '".', function () { + var identifier = objectUtils.parseKeyString(keyString); + var again = objectUtils.parseKeyString(identifier); + expect(identifier).toEqual(again); + again = objectUtils.parseKeyString(again); + again = objectUtils.parseKeyString(again); + expect(identifier).toEqual(again); + + var againKeyString = objectUtils.makeKeyString(again); + expect(againKeyString).toEqual(keyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + expect(againKeyString).toEqual(keyString); + }); + }); + }); + + describe('old object conversions', function () { + + it('translate ids', function () { + expect(objectUtils.toNewFormat({ + prop: 'someValue' + }, 'objId')) + .toEqual({ + prop: 'someValue', + identifier: { + namespace: '', + key: 'objId' + } + }); + }); + + it('translates composition', function () { + expect(objectUtils.toNewFormat({ + prop: 'someValue', + composition: [ + 'anotherObjectId', + 'scratch:anotherObjectId' + ] + }, 'objId')) + .toEqual({ + prop: 'someValue', + composition: [ + { + namespace: '', + key: 'anotherObjectId' + }, + { + namespace: 'scratch', + key: 'anotherObjectId' + } + ], + identifier: { + namespace: '', + key: 'objId' + } + }); + }); + }); + + describe('new object conversions', function () { + + it('removes ids', function () { + expect(objectUtils.toOldFormat({ + prop: 'someValue', + identifier: { + namespace: '', + key: 'objId' + } + })) + .toEqual({ + prop: 'someValue' + }); + }); + + it('translates composition', function () { + expect(objectUtils.toOldFormat({ + prop: 'someValue', + composition: [ + { + namespace: '', + key: 'anotherObjectId' + }, + { + namespace: 'scratch', + key: 'anotherObjectId' + } + ], + identifier: { + namespace: '', + key: 'objId' + } + })) + .toEqual({ + prop: 'someValue', + composition: [ + 'anotherObjectId', + 'scratch:anotherObjectId' + ] + }); + }); + }); + }); +});