From a2bcc828eb0d4767911f6abc9a91a8d02b8de212 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Fri, 17 Jun 2016 14:32:44 -0700 Subject: [PATCH] More hacks --- api/composition-api/CompositionAPI.js | 93 +++++++++++++++++ api/composition-api/README.md | 35 +++++++ api/composition-api/bundle.js | 47 +++++++++ api/object-api/ObjectAPI.js | 143 +++++++++++++------------- api/object-api/README.md | 105 ++++++++++++++++--- api/telemetry-api/bundle.js | 1 - main.js | 51 +-------- 7 files changed, 337 insertions(+), 138 deletions(-) create mode 100644 api/composition-api/CompositionAPI.js create mode 100644 api/composition-api/README.md create mode 100644 api/composition-api/bundle.js diff --git a/api/composition-api/CompositionAPI.js b/api/composition-api/CompositionAPI.js new file mode 100644 index 0000000000..5a16b7de96 --- /dev/null +++ b/api/composition-api/CompositionAPI.js @@ -0,0 +1,93 @@ +define([ + +], function ( + +) { + + + var PROVIDER_REGISTRY = []; + + function getProvider (object) { + return PROVIDER_REGISTRY.filter(function (p) { + return p.appliesTo(object); + })[0]; + }; + + function composition(object) { + var provider = getProvider(object); + + if (!provider) { + return; + } + + return new CompositionCollection(object, provider); + }; + + composition.addProvider = function (provider) { + PROVIDER_REGISTRY.unshift(provider); + }; + + window.MCT = window.MCT || {}; + window.MCT.composition = composition; + + function CompositionCollection(domainObject, provider) { + this.domainObject = domainObject; + this.provider = provider; + }; + + CompositionCollection.prototype.add = function (child, skipMutate) { + if (!this._children) { + throw new Error("Must load composition before you can add!"); + } + // we probably should not add until we have loaded. + // todo: should we modify parent? + if (!skipMutate) { + this.provider.add(this.domainObject, child); + } + this.children.push(child); + this.emit('add', child); + }; + + CompositionCollection.prototype.load = function () { + return this.provider.load(this.domainObject) + .then(function (children) { + this._children = []; + children.map(function (c) { + this.add(c, true); + }, this); + this.emit('load'); + // Todo: set up listener for changes via provider? + }.bind(this)); + }; + + CompositionCollection.prototype.remove = function (child) { + var index = this.children.indexOf(child); + if (index === -1) { + throw new Error("Unable to remove child: not found in composition"); + } + this.provider.remove(this.domainObject, child); + this.children.splice(index, 1); + this.emit('remove', index, child); + }; + + var DefaultCompositionProvider = { + appliesTo: function (domainObject) { + return !!domainObject.composition; + }, + load: function (domainObject) { + return Promise.all(domainObject.composition.map(MCT.objects.get)); + }, + add: function (domainObject, child) { + domainObject.composition.push(child.key); + } + }; + + composition.addProvider(DefaultCompositionProvider); + + function Injector() { + console.log('composition api injected!'); + } + + return Injector; + +}); diff --git a/api/composition-api/README.md b/api/composition-api/README.md new file mode 100644 index 0000000000..40d55d1a9a --- /dev/null +++ b/api/composition-api/README.md @@ -0,0 +1,35 @@ +# Composition API - Overview + +The composition API is straightforward: + +MCT.composition(object) -- returns a `CompositionCollection` if the object has +composition, returns undefined if it doesn't. + +## CompositionCollection + +Has three events: +* `load`: when the collection has completed loading. +* `add`: when a new object has been added to the collection. +* `remove` when an object has been removed from the collection. + +Has three methods: + +`Collection.load()` -- returns a promise that is fulfilled when the composition + has loaded. +`Collection.add(object)` -- add a domain object to the composition. +`Collection.remove(object)` -- remove the object from the composition. + +## Composition providers +composition providers are anything that meets the following interface: + +* `provider.appliesTo(domainObject)` -> return true if this provider can provide + composition for a given domain object. +* `provider.add(domainObject, childObject)` -> adds object +* `provider.remove(domainObject, childObject)` -> immediately removes objects +* `provider.load(domainObject)` -> returns promise for array of children + +There is a default composition provider which handles loading composition for +any object with a `composition` property. If you want specialized composition +loading behavior, implement your own composition provider and register it with + +`MCT.composition.addProvider(myProvider)` diff --git a/api/composition-api/bundle.js b/api/composition-api/bundle.js new file mode 100644 index 0000000000..232ed66d9d --- /dev/null +++ b/api/composition-api/bundle.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +/*global define*/ + +define([ + './CompositionAPI', + 'legacyRegistry' +], function ( + CompositionAPI, + legacyRegistry +) { + legacyRegistry.register('api/composition-api', { + name: 'Composition API', + description: 'The public Composition API', + extensions: { + runs: [ + { + key: "CompositionAPI", + priority: "mandatory", + implementation: CompositionAPI, + depends: [ + ] + } + ] + } + }); + +}); diff --git a/api/object-api/ObjectAPI.js b/api/object-api/ObjectAPI.js index 3886acfde6..1a474c9da9 100644 --- a/api/object-api/ObjectAPI.js +++ b/api/object-api/ObjectAPI.js @@ -16,17 +16,22 @@ define([ console.log(composition) }) }); - */ var Objects = {}, ROOT_REGISTRY = [], - PROVIDER_REGISTRY = []; + PROVIDER_REGISTRY = {}, + FALLBACK_PROVIDER; window.MCT = window.MCT || {}; window.MCT.objects = Objects; - function parseKey(key) { + // take a key string and turn it into a key object + // 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'} + function parseKeyString(key) { + if (typeof key === 'object') { + return key; + } var namespace = '', identifier = key; for (var i = 0, escaped = false, len=key.length; i < len; i++) { @@ -42,20 +47,40 @@ define([ }; }; - function makeKey(namespace, identifier) { - if (arguments.length === 1) { - identifier = namespace.identifier; - namespace = namespace.namespace; + // take a key and turn it into a key string + // {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root' + function makeKeyString(key) { + if (typeof key === 'string') { + return key; } - if (!namespace) { - return identifier + if (!key.namespace) { + return key.identifier; } return [ - namespace.replace(':', '\\:'), - identifier.replace(':', '\\:') + key.namespace.replace(':', '\\:'), + key.identifier.replace(':', '\\:') ].join(':'); }; + // Converts composition to use key strings instead of keys + function toOldFormat(model) { + delete model.key; + if (model.composition) { + model.composition = model.composition.map(makeKeyString); + } + return model; + }; + + // converts composition to use keys instead of key strings + function toNewFormat(model, key) { + model.key = key; + if (model.composition) { + model.composition = model.composition.map(parseKeyString); + } + return model; + }; + + // Root provider is hardcoded in; can't be skipped. var RootProvider = { 'get': function () { return Promise.resolve({ @@ -66,30 +91,26 @@ define([ } }; + // Retrieve the provider for a given key. function getProvider(key) { if (key.identifier === 'ROOT') { return RootProvider; } - return PROVIDER_REGISTRY.filter(function (p) { - return p.appliesTo(key); - })[0]; + return PROVIDER_REGISTRY[key.namespace] || FALLBACK_PROVIDER; }; - Objects.addProvider = function (provider) { - PROVIDER_REGISTRY.push(provider); + Objects.addProvider = function (namespace, provider) { + PROVIDER_REGISTRY[namespace] = provider; }; [ - 'create', 'save', 'delete', 'get' ].forEach(function (method) { Objects[method] = function () { var key = arguments[0], - keyParts = parseKey(key), - provider = getProvider(keyParts), - args = [keyParts].concat([].slice.call(arguments, 1)); + provider = getProvider(key); if (!provider) { throw new Error('No Provider Matched'); @@ -99,26 +120,20 @@ define([ throw new Error('Provider does not support [' + method + '].'); } - return provider[method].apply(provider, args); + return provider[method].apply(provider, arguments); }; }); - Objects.getComposition = function (object) { - if (!object.composition) { - throw new Error('object has no composition!'); - } - return Promise.all(object.composition.map(Objects.get, Objects)); + Objects.addRoot = function (key) { + ROOT_REGISTRY.unshift(key); }; - Objects.addRoot = function (keyParts) { - var key = makeKey(keyParts); - ROOT_REGISTRY.push(key); - }; - - Objects.removeRoot = function (keyParts) { - var key = makeKey(keyParts); + Objects.removeRoot = function (key) { ROOT_REGISTRY = ROOT_REGISTRY.filter(function (k) { - return k !== key; + return ( + k.identifier !== key.identifier || + k.namespace !== key.namespace + ); }); }; @@ -127,60 +142,42 @@ define([ this.instantiate = instantiate; } - ObjectServiceProvider.prototype.appliesTo = function (keyParts) { - return true; - }; - - ObjectServiceProvider.prototype.create = function (keyParts, object) { - var key = makeKey(keyParts), - object = this.instantiate(object, key); + ObjectServiceProvider.prototype.save = function (object) { + var key = object.key, + keyString = makeKeyString(key), + newObject = this.instantiate(toOldFormat(object), keyString); return object.getCapability('persistence') .persist() .then(function () { - return object; + return toNewFormat(object, key); }); }; - ObjectServiceProvider.prototype.save = function (keyParts, object) { - var key = makeKey(keyParts); - return this.objectService.getObjects([key]) - .then(function (results) { - var obj = results[key]; - obj.getCapability('mutation').mutate(function (model) { - _.extend(model, object); - }); - return obj.getCapability('persistence') - .persist() - .then(function () { - return object; - }); - }); - }; - - ObjectServiceProvider.prototype.delete = function (keyParts, object) { + ObjectServiceProvider.prototype.delete = function (object) { // TODO! }; - ObjectServiceProvider.prototype.get = function (keyParts) { - var key = makeKey(keyParts); - return this.objectService.getObjects([key]) + ObjectServiceProvider.prototype.get = function (key) { + var keyString = makeKeyString(key); + return this.objectService.getObjects([keyString]) .then(function (results) { - var model = results[key].getModel(); - if (model.composition) { - model.composition = model.composition.map(parseKey); - } - return model; + var model = JSON.parse(JSON.stringify(results[keyString].getModel())); + return toNewFormat(model, key); }); }; - function ObjectAPI(ROOTS, instantiate, objectService) { + // Injects new object API as a decorator so that it hijacks all requests. + // Object providers implemented on new API should just work, old API should just work, many things may break. + function ObjectAPIInjector(ROOTS, instantiate, objectService) { this.getObjects = function (keys) { var results = {}, - promises = keys.map(function (key) { + promises = keys.map(function (keyString) { + var key = parseKeyString(keyString); return Objects.get(key) .then(function (object) { - results[key] = instantiate(object, key); + object = toOldFormat(object) + results[keyString] = instantiate(object, keyString); }); }); @@ -190,14 +187,14 @@ define([ }); }; - PROVIDER_REGISTRY.push(new ObjectServiceProvider(objectService, instantiate)); + FALLBACK_PROVIDER = new ObjectServiceProvider(objectService, instantiate); ROOTS.forEach(function (r) { - ROOT_REGISTRY.push(r.id); + ROOT_REGISTRY.push(parseKeyString(r.id)); }); return this; } - return ObjectAPI; + return ObjectAPIInjector; }); diff --git a/api/object-api/README.md b/api/object-api/README.md index 1d066023ee..a7317944f9 100644 --- a/api/object-api/README.md +++ b/api/object-api/README.md @@ -2,23 +2,100 @@ The object API provides methods for fetching domain objects. +# Keys +Keys are a composite identifier that is used to create and persist objects. Ex: +```javascript +{ + namespace: 'elastic', + identifier: 'myIdentifier' +} +``` -### MCT.objects.get(namespace, identifier) +In old MCT days, we called this an "id", and we encoded it in a single string. +The above key would encode into the identifier, `elastic:myIdentifier`. -namespace, name, id (unique combination of namespace + name) +When interacting with the API you will be dealing with key objects. -### MCT.objects.Provider +# Configuring the Object API -provider.matchNamespace(namespace) -provider.get(id) -provider.save(domainObject) -provider.delete(domainObject) +The following methods should be used before calling run. They allow you to +configure the persistence space of MCT. - -### MCT.objects.save(domainObject) - Create, or +* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's + key. +* `MCT.objects.removeRoot(key)` -- Remove a "ROOT" from Open MCT by key. +* `MCT.objects.addProvider(namespace, provider)` -- register an object provider + for a specific namespace. See below for documentation on the provider + interface. -### MCT.objects.delete(id) - Delete an object by specifying id -### MCT.objects.delete(domainObject) - Delete an objects by specifying domain object \ No newline at end of file +# Using the object API + +The object API provides methods for getting, saving, and deleting objects. + +* MCT.objects.get(key) -> returns promise for an object +* MCT.objects.save(object) -> returns promise that is resolved when object + has been saved +* MCT.objects.delete(object) -> returns promise that is resolved when object has + been deleted + +## Configuration Example: Adding a groot + +The following example adds a new root object for groot and populates it with +some pieces of groot. + +```javascript + +var ROOT_KEY = { + namespace: 'groot', + identifier: 'groot' +}; + +var GROOT_ROOT = { + name: 'I am groot', + type: 'folder', + composition: [ + { + namespace: 'groot', + identifier: 'arms' + }, + { + namespace: 'groot', + identifier: 'legs' + }, + { + namespace: 'groot', + identifier: 'torso' + } + ] +}; + +var GrootProvider = { + get: function (key) { + if (key.identifier === 'groot') { + return Promise.resolve(GROOT_ROOT); + } + return Promise.resolve({ + name: 'Groot\'s ' + key.identifier + }); + } +}; + +MCT.objects.addRoot(ROOT_KEY); + +MCT.objects.addProvider('groot', GrootProvider); + +MCT.run(); +``` + +### Making a custom provider: + +All methods on the provider interface are optional, so you do not need +to modify them. + +* `provider.get(key)` -> promise for a domain object. +* `provider.save(domainObject)` -> returns promise that is fulfilled when object + has been saved. +* `provider.delete(domainObject)` -> returns promise that is fulfilled when + object has been deleted. + + diff --git a/api/telemetry-api/bundle.js b/api/telemetry-api/bundle.js index 2725285776..c540c618fb 100644 --- a/api/telemetry-api/bundle.js +++ b/api/telemetry-api/bundle.js @@ -43,5 +43,4 @@ define([ ] } }); - }); diff --git a/main.js b/main.js index 2b8f9f704f..a0d79c27d2 100644 --- a/main.js +++ b/main.js @@ -94,6 +94,7 @@ define([ './platform/commonUI/regions/bundle', './api/telemetry-api/bundle', './api/object-api/bundle', + './api/composition-api/bundle', './example/scratchpad/bundle', './example/imagery/bundle', @@ -102,56 +103,6 @@ define([ ], function (Main, legacyRegistry) { 'use strict'; - MCT.objects.addRoot({ - namespace: 'magic', - identifier: 'groot' - }); - - var GROOT_BITS = [ - { - key: { - namespace: 'magic', - identifier: 'groot-one' - }, - name: 'groot!' - }, - { - key: { - namespace: 'magic', - identifier: 'groot-two' - }, - name: 'groot?' - }, - { - key: { - namespace: 'magic', - identifier: 'groot-three' - }, - name: 'groot groot groot!' - } - ]; - - MCT.objects.addProvider({ - appliesTo: function (key) { - return key.namespace === 'magic'; - }, - get: function (key) { - if (key.identifier === 'groot') { - return Promise.resolve({ - 'name': 'grootman!', - 'type': 'folder', - 'composition': GROOT_BITS.map(function (gb) { - return gb.key - }) - }); - } - return GROOT_BITS.filter(function (gb) { - return (gb.key.identifier === key.identifier); - })[0]; - } - }); - - return { legacyRegistry: legacyRegistry, run: function () {