[Composition] Composition api improvements (#1332). Fixes #1322 and Fixes #1253

* [Composition] provide ids, sync via mutation

Composition provides ids, and we sync things via mutation.  This
simplifies the composition provider interface some, and also
fixes some issues with the previous default composition provider
related to #1322
fixes #1253

* [Style] Fix style, update jsdoc

Fix style, update jsdoc, clean up composition api changes for

Fixes #1322

* [Style] Tidy and JSDoc

* [Composition] Utilize new composition API

Ensures that composition provided by new API works in old API.

Some functionality is not present in both places, but for the
time being this is sufficient.

https://github.com/nasa/openmct/pull/1332

* [Utils] add tests, fix bugs

Add tests to objectUtils to ensure correctness.  This caught a bug
where character escapes were not properly applied or removed.  As
a result, any missing object that contained a colon in it's key
would cause an infinite loop and cause the application to crash.

Bug discovered in VISTA integration.

* [Style] Fix style

* [Roots] Depend on new api for ROOT

Depend on new API for ROOT model, ensuring consistency when
fetching ROOT model.

* [Style] Remove commented code
This commit is contained in:
Pete Richards 2016-11-30 12:00:01 -08:00 committed by Andrew Henry
parent 79b4f9a0f4
commit 73b3ae7264
13 changed files with 515 additions and 393 deletions

View File

@ -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",

View File

@ -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;
}
);

View File

@ -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"]);
});
});
}
);

View File

@ -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

View File

@ -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));
};

View File

@ -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);
};
}

View File

@ -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);
};
/**

View File

@ -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;

View File

@ -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.<Array.<module:openmct.DomainObject>>} a promise for
* the domain objects in this composition
* @returns {Promise.<Array.<module:openmct.Identifier>>} 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;

View File

@ -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

View File

@ -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;
}
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];
}
/**
* 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');
}
if (identifier === 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 = 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 (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
};
});

View File

@ -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']

View File

@ -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'
]
});
});
});
});
});