From 0fdce798f7b255deec9387cf9255af1ec6785916 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 20 Nov 2014 12:58:21 -0800 Subject: [PATCH] [Core] Bring in core bundle from sandbox Bring in bundle platform/core from the sandbox branch, in preparation for clean up, tests, and integration. WTD-573 --- platform/core/bundle.json | 147 +++++++++++ platform/core/src/actions/ActionAggregator.js | 30 +++ platform/core/src/actions/ActionCapability.js | 53 ++++ platform/core/src/actions/ActionProvider.js | 88 +++++++ platform/core/src/actions/CreateWizard.js | 73 ++++++ .../src/actions/LoggingActionDecorator.js | 46 ++++ platform/core/src/actions/create-action.js | 138 ++++++++++ platform/core/src/actions/duplicate-action.js | 235 ++++++++++++++++++ platform/core/src/actions/duplicate-wizard.js | 64 +++++ .../src/capabilities/CompositionCapability.js | 94 +++++++ .../src/capabilities/ContextCapability.js | 50 ++++ .../capabilities/ContextualDomainObject.js | 31 +++ .../capabilities/CoreCapabilityProvider.js | 63 +++++ .../src/capabilities/DelegationCapability.js | 57 +++++ .../src/capabilities/MutationCapability.js | 63 +++++ .../src/capabilities/PersistenceCapability.js | 38 +++ platform/core/src/models/ModelAggregator.js | 41 +++ .../core/src/models/PersistedModelProvider.js | 36 +++ platform/core/src/models/RootModelProvider.js | 41 +++ .../core/src/models/StaticModelProvider.js | 64 +++++ platform/core/src/objects/DomainObject.js | 107 ++++++++ .../core/src/objects/DomainObjectProvider.js | 85 +++++++ platform/core/src/types/MergeModels.js | 50 ++++ platform/core/src/types/TypeCapability.js | 27 ++ .../core/src/types/TypeCapabilityProvider.js | 113 +++++++++ platform/core/src/types/TypeImpl.js | 153 ++++++++++++ platform/core/src/types/TypeProperty.js | 117 +++++++++ .../core/src/types/TypePropertyConversion.js | 64 +++++ platform/core/src/types/TypeProvider.js | 185 ++++++++++++++ platform/core/src/types/TypeWizard.js | 70 ++++++ platform/core/src/views/ViewCapability.js | 25 ++ platform/core/src/views/ViewProvider.js | 55 ++++ platform/core/test/suite.json | 6 + .../test/types/TypeCapabilityProviderSpec.js | 13 + platform/core/test/types/TypeImplSpec.js | 72 ++++++ .../test/types/TypePropertyConversionSpec.js | 45 ++++ platform/core/test/types/TypePropertySpec.js | 61 +++++ platform/core/test/types/TypeProviderSpec.js | 214 ++++++++++++++++ 38 files changed, 2914 insertions(+) create mode 100644 platform/core/bundle.json create mode 100644 platform/core/src/actions/ActionAggregator.js create mode 100644 platform/core/src/actions/ActionCapability.js create mode 100644 platform/core/src/actions/ActionProvider.js create mode 100644 platform/core/src/actions/CreateWizard.js create mode 100644 platform/core/src/actions/LoggingActionDecorator.js create mode 100644 platform/core/src/actions/create-action.js create mode 100644 platform/core/src/actions/duplicate-action.js create mode 100644 platform/core/src/actions/duplicate-wizard.js create mode 100644 platform/core/src/capabilities/CompositionCapability.js create mode 100644 platform/core/src/capabilities/ContextCapability.js create mode 100644 platform/core/src/capabilities/ContextualDomainObject.js create mode 100644 platform/core/src/capabilities/CoreCapabilityProvider.js create mode 100644 platform/core/src/capabilities/DelegationCapability.js create mode 100644 platform/core/src/capabilities/MutationCapability.js create mode 100644 platform/core/src/capabilities/PersistenceCapability.js create mode 100644 platform/core/src/models/ModelAggregator.js create mode 100644 platform/core/src/models/PersistedModelProvider.js create mode 100644 platform/core/src/models/RootModelProvider.js create mode 100644 platform/core/src/models/StaticModelProvider.js create mode 100644 platform/core/src/objects/DomainObject.js create mode 100644 platform/core/src/objects/DomainObjectProvider.js create mode 100644 platform/core/src/types/MergeModels.js create mode 100644 platform/core/src/types/TypeCapability.js create mode 100644 platform/core/src/types/TypeCapabilityProvider.js create mode 100644 platform/core/src/types/TypeImpl.js create mode 100644 platform/core/src/types/TypeProperty.js create mode 100644 platform/core/src/types/TypePropertyConversion.js create mode 100644 platform/core/src/types/TypeProvider.js create mode 100644 platform/core/src/types/TypeWizard.js create mode 100644 platform/core/src/views/ViewCapability.js create mode 100644 platform/core/src/views/ViewProvider.js create mode 100644 platform/core/test/suite.json create mode 100644 platform/core/test/types/TypeCapabilityProviderSpec.js create mode 100644 platform/core/test/types/TypeImplSpec.js create mode 100644 platform/core/test/types/TypePropertyConversionSpec.js create mode 100644 platform/core/test/types/TypePropertySpec.js create mode 100644 platform/core/test/types/TypeProviderSpec.js diff --git a/platform/core/bundle.json b/platform/core/bundle.json new file mode 100644 index 0000000000..f7197a361c --- /dev/null +++ b/platform/core/bundle.json @@ -0,0 +1,147 @@ +{ + "name": "Open MCT Web Core", + "description": "Defines core concepts of Open MCT Web.", + "sources": "src", + "extensions": { + "components": [ + { + "provides": "objectService", + "type": "provider", + "implementation": "objects/DomainObjectProvider.js", + "depends": [ "modelService", "capabilityService" ] + }, + { + "provides": "capabilityService", + "type": "provider", + "implementation": "capabilities/CoreCapabilityProvider.js", + "depends": [ "capabilities[]" ] + }, + { + "provides": "modelService", + "type": "provider", + "implementation": "models/StaticModelProvider.js", + "depends": [ "models[]", "$log" ] + }, + { + "provides": "modelService", + "type": "provider", + "implementation": "models/RootModelProvider.js", + "depends": [ "roots[]", "$log" ] + }, + { + "provides": "modelService", + "type": "aggregator", + "implementation": "models/ModelAggregator.js" + }, + { + "provides": "modelService", + "type": "provider", + "implementation": "models/PersistedModelProvider.js", + "depends": [ "persistenceService", "$q", "PERSISTENCE_SPACE" ] + }, + { + "provides": "typeService", + "type": "provider", + "implementation": "types/TypeProvider.js", + "depends": [ "types[]" ] + }, + { + "provides": "actionService", + "type": "provider", + "implementation": "actions/ActionProvider.js", + "depends": [ "actions[]" ] + }, + { + "provides": "actionService", + "type": "aggregator", + "implementation": "actions/ActionAggregator.js" + }, + { + "provides": "actionService", + "type": "decorator", + "implementation": "actions/LoggingActionDecorator.js", + "depends": [ "$log" ] + }, + { + "provides": "viewService", + "type": "provider", + "implementation": "views/ViewProvider.js", + "depends": [ "views[]" ] + } + ], + "types": [ + { + "properties": [ + { + "control": "_textfield", + "label": "Title", + "key": "name", + "property": "name", + "pattern": "\\S+", + "required": true + }, + { + "control": "_checkbox", + "label": "Display title by default", + "key": "displayTitle", + "property": [ "display", "title" ] + } + ] + }, + { + "key": "folder", + "name": "Folder", + "glyph": "F", + "features": "creation", + "description": "A folder, useful for storing and organizing domain objects.", + "model": { "composition": [] } + } + ], + "capabilities": [ + { + "key": "composition", + "implementation": "capabilities/CompositionCapability.js", + "depends": [ "$injector" ] + }, + { + "key": "type", + "implementation": "types/TypeCapability.js", + "depends": [ "typeService" ] + }, + { + "key": "action", + "implementation": "actions/ActionCapability.js", + "depends": [ "actionService" ] + }, + { + "key": "view", + "implementation": "views/ViewCapability.js", + "depends": [ "viewService" ] + }, + { + "key": "persistence", + "implementation": "capabilities/PersistenceCapability.js", + "depends": [ "persistenceService", "PERSISTENCE_SPACE" ] + }, + { + "key": "mutation", + "implementation": "capabilities/MutationCapability.js", + "depends": [ "$q" ] + }, + { + "key": "delegation", + "implementation": "capabilities/DelegationCapability.js" + } + ], + "roots": [ + { + "id": "mine", + "model": { + "name": "My Items", + "type": "folder", + "composition": [] + } + } + ] + } +} \ No newline at end of file diff --git a/platform/core/src/actions/ActionAggregator.js b/platform/core/src/actions/ActionAggregator.js new file mode 100644 index 0000000000..319fdf684d --- /dev/null +++ b/platform/core/src/actions/ActionAggregator.js @@ -0,0 +1,30 @@ +/*global define,Promise*/ + +define( + function () { + "use strict"; + + /** + * An action service factory which aggregates + * multiple other action services. + */ + function ActionAggregator(actionProviders) { + + function getActions(context) { + // Get all actions from all providers, reduce down + // to one array by concatenation + return actionProviders.map(function (provider) { + return provider.getActions(context); + }).reduce(function (a, b) { + return a.concat(b); + }, []); + } + + return { + getActions: getActions + }; + } + + return ActionAggregator; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/ActionCapability.js b/platform/core/src/actions/ActionCapability.js new file mode 100644 index 0000000000..07a576a6c6 --- /dev/null +++ b/platform/core/src/actions/ActionCapability.js @@ -0,0 +1,53 @@ +/*global define,Promise*/ + +/** + * Module defining ActionCapability. Created by vwoeltje on 11/10/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ActionCapability(actionService, domainObject) { + var self; + + function getActions(context) { + var baseContext = typeof context === 'string' ? + { key: context } : + (context || {}), + actionContext = Object.create(baseContext); + + actionContext.domainObject = domainObject; + + return actionService.getActions(actionContext); + } + + function performAction(context) { + var actions = getActions(context); + + return Promise.resolve( + (actions && actions.length > 0) ? + actions[0].perform() : + undefined + ); + } + + self = { + invoke: function () { + return self; + }, + perform: performAction, + getActions: getActions + + }; + + return self; + } + + return ActionCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/ActionProvider.js b/platform/core/src/actions/ActionProvider.js new file mode 100644 index 0000000000..4d8821bd05 --- /dev/null +++ b/platform/core/src/actions/ActionProvider.js @@ -0,0 +1,88 @@ +/*global define,Promise*/ + +/** + * Module defining ActionProvider. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ActionProvider(actions) { + var actionsByKey = {}, + actionsByCategory = {}; + + function instantiateAction(Action, context) { + var action = new Action(context), + metadata; + + // Provide a getMetadata method that echos + // declarative bindings, as well as context, + // unless the action has defined its own. + if (!action.getMetadata) { + metadata = Object.create(Action.definition); + metadata.context = context; + action.getMetadata = function () { + return metadata; + }; + } + + return action; + } + + function createIfApplicable(actions, context) { + return (actions || []).filter(function (Action) { + return Action.appliesTo ? + Action.appliesTo(context) : true; + }).map(function (Action) { + return instantiateAction(Action, context); + }); + } + + function getActions(actionContext) { + var context = (actionContext || {}), + category = context.category, + key = context.key, + candidates; + + candidates = actions; + if (key) { + candidates = actionsByKey[key]; + if (category) { + candidates = candidates.filter(function (Action) { + return Action.category === category; + }); + } + } else if (category) { + candidates = actionsByCategory[category]; + } + + return createIfApplicable(candidates, context); + } + + // Build up look-up tables + actions.forEach(function (Action) { + if (Action.category) { + actionsByCategory[Action.category] = + actionsByCategory[Action.category] || []; + actionsByCategory[Action.category].push(Action); + } + if (Action.key) { + actionsByKey[Action.key] = + actionsByKey[Action.key] || []; + actionsByKey[Action.key].push(Action); + } + }); + + return { + getActions: getActions + }; + } + + return ActionProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/CreateWizard.js b/platform/core/src/actions/CreateWizard.js new file mode 100644 index 0000000000..708f46a845 --- /dev/null +++ b/platform/core/src/actions/CreateWizard.js @@ -0,0 +1,73 @@ +/*global define*/ + +/** + * Defines the CreateWizard, used by the CreateAction to + * populate the form shown in dialog based on the created type. + * + * @module core/action/create-wizard + */ +define( + function () { + 'use strict'; + + /** + * Construct a new CreateWizard. + * + * @param {TypeImpl} type the type of domain object to be created + * @param {DomainObject} parent the domain object to serve as + * the initial parent for the created object, in the dialog + * @constructor + * @memberof module:core/action/create-wizard + */ + function CreateWizard(type, parent) { + var model = type.getInitialModel(), + properties = type.getProperties(); + + return { + getSections: function () { + var parentRow = Object.create(parent), + sections = []; + + sections.push({ + label: "Properties", + rows: properties.map(function (property) { + // Property definition is same as form row definition + var row = Object.create(property.getDefinition()); + // But pull an initial value from the model + row.value = property.getValue(model); + return row; + }) + }); + + // Ensure there is always a "save in" section + parentRow.label = "Save In"; + parentRow.cssclass = "selector-list"; + parentRow.control = "_locator"; + parentRow.key = "createParent"; + sections.push({ label: 'Location', rows: [parentRow]}); + + return sections; + }, + createModel: function (formValue) { + // Clone + var newModel = JSON.parse(JSON.stringify(model)); + + // Always use the type from the type definition + newModel.type = type.getKey(); + + // Update all properties + properties.forEach(function (property) { + var value = formValue[property.getDefinition().key]; + property.setValue(newModel, value); + }); + + return newModel; + } + }; + + + } + + return CreateWizard; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/LoggingActionDecorator.js b/platform/core/src/actions/LoggingActionDecorator.js new file mode 100644 index 0000000000..4f3da0af89 --- /dev/null +++ b/platform/core/src/actions/LoggingActionDecorator.js @@ -0,0 +1,46 @@ +/*global define,Promise*/ + +/** + * Module defining LoggingActionDecorator. Created by vwoeltje on 11/17/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function LoggingActionDecorator($log, actionService) { + function addLogging(action) { + var logAction = Object.create(action), + domainObject = + action.getMetadata().context.domainObject; + + logAction.perform = function () { + $log.info([ + "Performing action ", + action.getMetadata().key, + " upon ", + domainObject && domainObject.getId() + ].join("")); + return action.perform.apply(action, arguments); + }; + + return logAction; + } + + return { + getActions: function () { + return actionService.getActions.apply( + actionService, + arguments + ).map(addLogging); + } + }; + } + + return LoggingActionDecorator; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/create-action.js b/platform/core/src/actions/create-action.js new file mode 100644 index 0000000000..11f55198dc --- /dev/null +++ b/platform/core/src/actions/create-action.js @@ -0,0 +1,138 @@ +/*global define*/ + +/** + * Implements the Create action, which users may utilize to + * create new domain objects. + * + * @module core/action/create-action + */ +define( + ['core/promises', 'core/action/create-wizard', 'uuid'], + function (promises, CreateWizard, uuid) { + "use strict"; + + // Handles issuing Navigate actions in order to + // display a newly-created object immediately. + function navigateTo(id, parent) { + // Look up child objects... + promises.decorate( + parent.getCapability('composition'), + function (c) { return c.list(); } + ).then( + function (composition) { + // ...and find one with a matching id... + composition.forEach(function (child) { + if (child.getId() === id) { + // ...and navigate to it. + child.getCapability('action').then( + function (action) { + action.performAction('navigate', {}); + } + ); + } + }); + } + ); + } + + /** + * Instantiate a new Create action for a specified type. + * + * @param {ActionContext} context the context in which the action would occur + * @param {module:core/type/type-impl.Type} type the type of domain object + * to be createdxs + * @param {DialogService} dialogService the service used to display the + * Create dialog + * @param {PersistenceService} persistenceService the service used to + * persist the model of the newly-created domain object + * @param {string} spaceName the name of the space in which to store + * the created object; used when communicating with the + * persistence service. + * @constructor CreateAction + * @memberof module:core/action/create-action + */ + return function CreateAction(context, type, dialogService, persistenceService, spaceName) { + + // Invoked when the Create Action is performed + function perform() { + var id = uuid(), // get a unique id for the new object + parent = context.selection[0].object, + wizard = new CreateWizard(type, parent); + + // Pop up the create dialog + dialogService.getUserInput( + {sections: wizard.getSections()}, + "Create a New " + type.getName() + ).then(function (userInput) { + // userInput will be undefined if cancelled + if (userInput) { + userInput.type = userInput.type || type.getKey(); + + // Create and persist the model for the new object. + // Note that we rely upon the persistence service + // being wired such that this model will be available + // via a model service. + persistenceService.createObject( + spaceName, + id, + wizard.createModel(userInput) + ).then(function (result) { + var model, + destination = userInput.createParent || parent; + if (result) { + // Mutate the containing object: Add the newly + // created object as part of its composition. + destination.getCapability('mutation').then( + function (mutation) { + return mutation.mutate(function (model) { + model.composition = model.composition || []; + model.composition.push(id); + return model; + }); + } + ).then(function (mutated) { + // Persist immediately (persistence upon mutation + // is not automatic, to permit "working copy" + // changes to objects, as in edit mode) + if (mutated) { + destination.getCapability('persistence').then( + function (persistence) { + promises.as( + persistence.persist() + ).then(function () { + navigateTo(id, destination); + }); + } + ); + } + }); + } + }); + } + }); + + + } + + function metadata() { + return { + id: 'create-' + type.getKey(), + name: type.getName(), + category: 'create', + glyph: type.getGlyph(), + context: context, + description: type.getDescription() + }; + } + + + return { + perform: perform, + metadata: metadata + }; + + + }; + + } +); \ No newline at end of file diff --git a/platform/core/src/actions/duplicate-action.js b/platform/core/src/actions/duplicate-action.js new file mode 100644 index 0000000000..6e71a2a7cd --- /dev/null +++ b/platform/core/src/actions/duplicate-action.js @@ -0,0 +1,235 @@ +/*global define,Promise*/ + +/** + * Module defining duplicate action. Created by vwoeltje on 11/5/14. + */ +define( + ['core/promises', 'core/action/duplicate-wizard', 'uuid'], + function (promises, DuplicateWizard, uuid) { + "use strict"; + + /** + * Deep-copy a domain object and its composition. + * @constructor + */ + function DuplicateAction(context, dialogService, persistenceService, spaceName) { + var object = context.selection[0].object; + + function makeWizard(parameters) { + return new DuplicateWizard( + object.getModel(), + parameters.type, + parameters.parent + ); + } + + function getParent() { + return object.getCapability('context').then(function (c) { + return c.getParent(); + }); + } + + function showDialog(wizard) { + return dialogService.getUserInput( + { sections: wizard.getSections() }, + "Duplicate " + object.getModel().name + ); + } + + function getComposition(object) { + return object.getCapability('composition').then( + function (c) { return c.list(); }, + function () { return []; } + ); + } + + function getModels() { + var models = {}; + + function populateModelsFor(object) { + var id = object.getId(); + + // Already stored this object, don't keep going + if (models[id]) { + return models; + } + + // Clone to new map + models[id] = JSON.parse(JSON.stringify(object.getModel())); + + return getComposition(object).then(function (objs) { + return promises.merge(objs.map(populateModelsFor)); + }); + } + + return populateModelsFor(object).then(function () { + return models; + }); + } + + function buildIdMap(ids) { + var idMap = {}; + + ids.forEach(function (id) { + idMap[id] = uuid(); + }); + + return idMap; + } + + function rewriteComposition(models, idMap) { + Object.keys(models).forEach(function (id) { + if (models[id].composition) { + models[id].composition = models[id].composition.map(function (childId) { + return idMap[childId] || childId; + }); + } + }); + return models; + } + + function shouldRewrite(state, idMap) { + var keys; + + function isId(key) { + return Object.prototype.hasOwnProperty.apply(idMap, [key]); + } + + function and(a, b) { + return a && b; + } + + keys = Object.keys(state); + + return keys.length > 0 && keys.map(isId).reduce(and, true); + } + + function rewriteIdentifierKeys(state, idMap) { + if (typeof state !== 'object' || state === null) { + return state; + } + + if (shouldRewrite(state, idMap)) { + // Rewrite the keys of a JavaScript object + Object.keys(state).forEach(function (id) { + var newId = idMap[id] || id, + oldState = state[id]; + delete state[id]; + state[newId] = oldState; + }); + } + + // Recursively search for model contents which + // look like id maps + Object.keys(state).forEach(function (k) { + state[k] = rewriteIdentifierKeys(state[k], idMap); + }); + + return state; + } + + function rewriteIdentifiers(models, idMap) { + var newModels = {}; + + Object.keys(models).forEach(function (id) { + var newId = idMap[id] || id; + newModels[newId] = models[id]; + }); + + return newModels; + } + + function doPersist(models) { + var ids = Object.keys(models); + return promises.merge(ids.map(function (id) { + return persistenceService.createObject( + spaceName, + id, + models[id] + ); + })); + } + + function doDuplicate(newModel) { + var idMap; + + if (!newModel) { + return undefined; + } + + return getModels().then(function (models) { + // Add in the model from user input + models[object.getId()] = newModel; + idMap = buildIdMap(Object.keys(models)); + + rewriteComposition(models, idMap); + models = rewriteIdentifiers(models, idMap); + return rewriteIdentifierKeys(models, idMap); + }).then(doPersist).then(function () { + // Return the new identifier for the object + return idMap[object.getId()]; + }); + } + + + function addToComposition(destination, id) { + function mutator(model) { + if (model.composition) { + model.composition.push(id); + } + } + + return destination.getCapability('mutation').then( + function (m) { return m.mutate(mutator); } + ).then(function () { + return destination.getCapability('persistence'); + }).then(function (p) { + return p.persist(); + }); + } + + function perform() { + var destination, wizard; + + // Pop up the create dialog + promises.merge({ + type: object.getCapability('type'), + parent: getParent() + }).then(function (params) { + // Record parent, to add to composition later + destination = params.parent; + return params; + }).then(function (params) { + wizard = makeWizard(params); + return wizard; + }).then(showDialog).then(function (formValue) { + // If user picked a different destination, use that + if (formValue && formValue.createParent) { + destination = formValue.createParent; + } + return formValue && wizard.createModel(formValue); + }).then(doDuplicate).then(function (newId) { + return addToComposition(destination, newId); + }); + } + + function metadata() { + return { + id: 'duplicate-' + object.getId(), + name: "Duplicate...", + category: 'contextual', + glyph: "+", + context: context, + description: "Make a copy of this object, and its contained objects." + }; + } + + return { + perform: perform, + metadata: metadata + }; + } + + return DuplicateAction; + } +); \ No newline at end of file diff --git a/platform/core/src/actions/duplicate-wizard.js b/platform/core/src/actions/duplicate-wizard.js new file mode 100644 index 0000000000..d44827f5c2 --- /dev/null +++ b/platform/core/src/actions/duplicate-wizard.js @@ -0,0 +1,64 @@ +/*global define,Promise*/ + +/** + * Module defining duplicate-wizard. Created by vwoeltje on 11/5/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function DuplicateWizard(model, type, parent) { + var properties = type.getProperties(); + + return { + getSections: function () { + var parentRow = Object.create(parent), + sections = []; + + sections.push({ + label: "Properties", + rows: properties.map(function (property) { + // Property definition is same as form row definition + var row = Object.create(property.getDefinition()); + // But pull an initial value from the model + row.value = property.getValue(model); + return row; + }) + }); + + // Ensure there is always a "save in" section + parentRow.label = "Save In"; + parentRow.cssclass = "selector-list"; + parentRow.control = "_locator"; + parentRow.key = "createParent"; + sections.push({ label: 'Location', rows: [parentRow]}); + + return sections; + }, + createModel: function (formValue) { + // Clone + var newModel = JSON.parse(JSON.stringify(model)); + + // Always use the type from the type definition + newModel.type = type.getKey(); + + // Update all properties + properties.forEach(function (property) { + var value = formValue[property.getDefinition().key]; + property.setValue(newModel, value); + }); + + return newModel; + } + }; + + } + + return DuplicateWizard; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/CompositionCapability.js b/platform/core/src/capabilities/CompositionCapability.js new file mode 100644 index 0000000000..2b8f0609dc --- /dev/null +++ b/platform/core/src/capabilities/CompositionCapability.js @@ -0,0 +1,94 @@ +/*global define,Promise*/ + +/** + * Module defining CompositionCapability. Created by vwoeltje on 11/7/14. + */ +define( + ["./ContextualDomainObject"], + function (ContextualDomainObject) { + "use strict"; + + /** + * Composition capability. A domain object's composition is the set of + * domain objects it contains. This is available as an array of + * identifiers in the model; the composition capability makes this + * available as an array of domain object instances, which may + * require consulting the object service (e.g. to trigger a database + * query to retrieve the nested object models.) + * + * @constructor + */ + function CompositionCapability($injector, domainObject) { + var objectService, + lastPromise, + lastModified; + + // Get a reference to the object service from $injector + function injectObjectService() { + objectService = $injector.get("objectService"); + return objectService; + } + + // Get a reference to the object service (either cached or + // from the injector) + function getObjectService() { + return objectService || injectObjectService(); + } + + // Promise this domain object's composition (an array of domain + // object instances corresponding to ids in its model.) + function promiseComposition() { + var model = domainObject.getModel(), + ids; + + // Then filter out non-existent objects, + // and wrap others (such that they expose a + // "context" capability) + function contextualize(objects) { + return ids.filter(function (id) { + return objects[id]; + }).map(function (id) { + return new ContextualDomainObject( + objects[id], + domainObject + ); + }); + } + + // Make a new request if we haven't made one, or if the + // object has been modified. + if (!lastPromise || lastModified !== model.modified) { + ids = model.composition || []; + lastModified = model.modified; + // Load from the underlying object service + lastPromise = getObjectService().getObjects(ids) + .then(contextualize); + } + + return lastPromise; + } + + return { + /** + * Request the composition of this object. + * @returns {Promise.} a list of all domain + * objects which compose this domain object. + */ + invoke: promiseComposition + }; + } + + /** + * Test to determine whether or not this capability should be exposed + * by a domain object based on its model. Checks for the presence of + * a composition field, that must be an array. + * @param model the domain object model + * @returns {boolean} true if this object has a composition + */ + CompositionCapability.appliesTo = function (model) { + return Array.isArray((model || {}).composition); + }; + + return CompositionCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/ContextCapability.js b/platform/core/src/capabilities/ContextCapability.js new file mode 100644 index 0000000000..a6074c4b76 --- /dev/null +++ b/platform/core/src/capabilities/ContextCapability.js @@ -0,0 +1,50 @@ +/*global define,Promise*/ + +/** + * Module defining ContextCapability. Created by vwoeltje on 11/17/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ContextCapability(parentObject, domainObject) { + var self, + parentObject; + + self = { + getParent: function () { + return parentObject; + }, + getPath: function () { + var parentPath = [], + parentContext; + + if (parentObject) { + parentContext = parentObject.getCapability("context"); + parentPath = parentContext ? + parentContext.getPath() : + [parentObject]; + } + + return parentPath.concat([domainObject]); + }, + getRoot: function () { + return self.getPath()[0]; + } + }; + + return self; + } + + ContextCapability.appliesTo = function () { + return true; + }; + + return ContextCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/ContextualDomainObject.js b/platform/core/src/capabilities/ContextualDomainObject.js new file mode 100644 index 0000000000..eeb5b9cf3d --- /dev/null +++ b/platform/core/src/capabilities/ContextualDomainObject.js @@ -0,0 +1,31 @@ +/*global define,Promise*/ + +/** + * Module defining ContextualDomainObject. Created by vwoeltje on 11/18/14. + */ +define( + ["./ContextCapability"], + function (ContextCapability) { + "use strict"; + + /** + * + * @constructor + */ + function ContextualDomainObject(domainObject, parentObject) { + var contextualObject = Object.create(domainObject), + contextCapability = + new ContextCapability(parentObject, domainObject); + + contextualObject.getCapability = function (name) { + return name === "context" ? + contextCapability : + domainObject.getCapability.apply(this, arguments); + }; + + return contextualObject; + } + + return ContextualDomainObject; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/CoreCapabilityProvider.js b/platform/core/src/capabilities/CoreCapabilityProvider.js new file mode 100644 index 0000000000..4036ccf3e8 --- /dev/null +++ b/platform/core/src/capabilities/CoreCapabilityProvider.js @@ -0,0 +1,63 @@ +/*global define,Promise*/ + +/** + * Module defining CoreCapabilityProvider. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * Provides capabilities based on extension definitions. + * @constructor + */ + function CoreCapabilityProvider(capabilities) { + // Filter by invoking the capability's appliesTo method + function filterCapabilities(model) { + return capabilities.filter(function (capability) { + return capability.appliesTo ? + capability.appliesTo(model) : + true; + }); + } + + // Package capabilities as key-value pairs + function packageCapabilities(capabilities) { + var result = {}; + capabilities.forEach(function (capability) { + result[capability.key] = capability; + }); + return result; + } + + function getCapabilities(model) { + return packageCapabilities(filterCapabilities(model)); + } + + return { + /** + * Get all capabilities associated with a given domain + * object. + * + * This returns a promise for an object containing key-value + * pairs, where keys are capability names and values are + * either: + * + * * Capability instances + * * Capability constructors (which take a domain object + * as their argument.) + * + * + * @param {*} model the object model + * @returns {Object.} all + * capabilities known to be valid for this model, as + * key-value pairs + */ + getCapabilities: getCapabilities + }; + } + + return CoreCapabilityProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/DelegationCapability.js b/platform/core/src/capabilities/DelegationCapability.js new file mode 100644 index 0000000000..d4a1317460 --- /dev/null +++ b/platform/core/src/capabilities/DelegationCapability.js @@ -0,0 +1,57 @@ +/*global define,Promise*/ + +/** + * Module defining DelegationCapability. Created by vwoeltje on 11/18/14. + */ +define( + [], + function () { + 'use strict'; + + function DelegationCapability(domainObject) { + var delegateCapabilities = {}, + type = domainObject.getCapability("type"); + + function filterObjectsWithCapability(capability) { + return function (objects) { + return objects.filter(function (obj) { + return obj.hasCapability(capability); + }); + }; + } + + function promiseChildren() { + return domainObject.useCapability('composition'); + } + + function doesDelegate(key) { + return delegateCapabilities[key] || false; + } + + function getDelegates(capability) { + return doesDelegate(capability) ? + promiseChildren().then( + filterObjectsWithCapability(capability) + ) : + []; + } + + // Generate set for easy lookup of capability delegation + if (type && type.getDefinition) { + (type.getDefinition().delegates || []).forEach(function (key) { + delegateCapabilities[key] = true; + }); + } + + return { + invoke: getDelegates, + getDelegates: getDelegates, + doesDelegateCapability: doesDelegate + }; + } + + + return DelegationCapability; + + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/MutationCapability.js b/platform/core/src/capabilities/MutationCapability.js new file mode 100644 index 0000000000..6ca6f469a6 --- /dev/null +++ b/platform/core/src/capabilities/MutationCapability.js @@ -0,0 +1,63 @@ +/*global define,Promise*/ + +/** + * Module defining MutationCapability. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + // Utility function to overwrite a destination object + // with the contents of a source object. + function copyValues(destination, source) { + // First, remove all previously-existing keys + Object.keys(destination).forEach(function (k) { + delete destination[k]; + }); + // Second, write all new keys + Object.keys(source).forEach(function (k) { + destination[k] = source[k]; + }); + } + + function MutationCapability($q, domainObject) { + + function mutate(mutator) { + // Get the object's model and clone it, so the + // mutator function has a temporary copy to work with. + var model = domainObject.getModel(), + clone = JSON.parse(JSON.stringify(model)); + + // Function to handle copying values to the actual + function handleMutation(mutationResult) { + // If mutation result was undefined, just use + // the clone; this allows the mutator to omit return + // values and just change the model directly. + var result = mutationResult || clone; + + // Allow mutators to change their mind by + // returning false. + if (mutationResult !== false) { + copyValues(model, result); + model.modified = Date.now(); + } + + // Report the result of the mutation + return mutationResult !== false; + } + + // Invoke the provided mutator, then make changes to + // the underlying model (if applicable.) + return $q.when(mutator(clone)) + .then(handleMutation); + } + + return { + invoke: mutate + }; + } + + return MutationCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/capabilities/PersistenceCapability.js b/platform/core/src/capabilities/PersistenceCapability.js new file mode 100644 index 0000000000..96d5f4f378 --- /dev/null +++ b/platform/core/src/capabilities/PersistenceCapability.js @@ -0,0 +1,38 @@ +/*global define*/ + +/** + * Defines the "persistence" capability, used to indicate + * that changes to an object should be written to some + * underlying store. + * + * Current implementation is a stub that simply triggers + * a refresh on modified views, which is a necessary + * side effect of persisting the object. + */ +define( + function () { + 'use strict'; + + function PersistenceCapability(persistenceService, SPACE, domainObject) { + var self = { + persist: function () { + return persistenceService.updateObject( + SPACE, + domainObject.getId(), + domainObject.getModel() + ); + }, + getSpace: function () { + return SPACE; + }, + invoke: function () { + return self; + } + }; + + return self; + } + + return PersistenceCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/models/ModelAggregator.js b/platform/core/src/models/ModelAggregator.js new file mode 100644 index 0000000000..dcbc84a5c2 --- /dev/null +++ b/platform/core/src/models/ModelAggregator.js @@ -0,0 +1,41 @@ +/*global define,Promise*/ + +/** + * Module defining ModelAggregator. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ModelAggregator(providers) { + function mergeModels(provided, ids) { + var result = {}; + ids.forEach(function (id) { + provided.forEach(function (models) { + if (models[id]) { + result[id] = models[id]; + } + }); + }); + return result; + } + + return { + getModels: function (ids) { + return Promise.all(providers.map(function (provider) { + return provider.getModels(ids); + })).then(function (provided) { + return mergeModels(provided, ids); + }); + } + }; + } + + return ModelAggregator; + } +); \ No newline at end of file diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js new file mode 100644 index 0000000000..eda8409b52 --- /dev/null +++ b/platform/core/src/models/PersistedModelProvider.js @@ -0,0 +1,36 @@ +/*global define,Promise*/ + +/** + * Module defining PersistedModelProvider. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function PersistedModelProvider(persistenceService, $q, SPACE) { + function promiseModels(ids) { + return $q.all(ids.map(function (id) { + return persistenceService.readObject(SPACE, id); + })).then(function (models) { + var result = {}; + ids.forEach(function (id, index) { + result[id] = models[index]; + }); + return result; + }); + } + + return { + getModels: promiseModels + }; + } + + + return PersistedModelProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/models/RootModelProvider.js b/platform/core/src/models/RootModelProvider.js new file mode 100644 index 0000000000..8ea9709ab1 --- /dev/null +++ b/platform/core/src/models/RootModelProvider.js @@ -0,0 +1,41 @@ +/*global define,Promise*/ + +/** + * Module defining RootModelProvider. Created by vwoeltje on 11/7/14. + */ +define( + ['./StaticModelProvider.js'], + function (StaticModelProvider) { + "use strict"; + + /** + * 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. + * + * @constructor + */ + function RootModelProvider(roots, $log) { + var ids = roots.map(function (root) { + return root.id; + }), + baseProvider = new StaticModelProvider(roots, $log); + + function addRoot(models) { + models.ROOT = { + name: "The root object", + composition: ids + }; + return models; + } + + return { + getModels: function(ids) { + return baseProvider.getModels(ids).then(addRoot); + } + }; + } + + return RootModelProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/models/StaticModelProvider.js b/platform/core/src/models/StaticModelProvider.js new file mode 100644 index 0000000000..4a434ae9ac --- /dev/null +++ b/platform/core/src/models/StaticModelProvider.js @@ -0,0 +1,64 @@ +/*global define,Promise*/ + +/** + * Module defining StaticModelProvider. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * Loads static models, provided as declared extensions of bundles. + * @constructor + */ + function StaticModelProvider(models, $log) { + var modelMap = {}; + + function addModelToMap(model) { + // Skip models which don't look right + if (typeof model !== 'object' || + typeof model.id !== 'string' || + typeof model.model !== 'object') { + $log.warn([ + "Skipping malformed domain object model exposed by ", + ((model || {}).bundle || {}).path + ].join("")); + } else { + modelMap[model.id] = model.model; + } + } + + // Prepoulate maps with models to make subsequent lookup faster. + models.forEach(addModelToMap); + + return { + /** + * Get models for these specified string identifiers. + * These will be given as an object containing keys + * and values, where keys are object identifiers and + * values are models. + * This result may contain either a subset or a + * superset of the total objects. + * + * @param {Array} ids the string identifiers for + * models of interest. + * @returns {Promise} a promise for an object + * containing key-value pairs, where keys are + * ids and values are models + * @method + * @memberof StaticModelProvider# + */ + getModels: function (ids) { + var result = {}; + ids.forEach(function (id) { + result[id] = modelMap[id]; + }); + return Promise.resolve(result); + } + }; + } + + return StaticModelProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/objects/DomainObject.js b/platform/core/src/objects/DomainObject.js new file mode 100644 index 0000000000..1806cb8d23 --- /dev/null +++ b/platform/core/src/objects/DomainObject.js @@ -0,0 +1,107 @@ +/*global define,Promise*/ + +/** + * Module defining DomainObject. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * Construct a new domain object with the specified + * identifier, model, and capabilities. + * + * @param {string} id the object's unique identifier + * @param {object} model the "JSONifiable" state of the object + * @param {Object.} ids the identifiers for domain objects + * of interest. + * @return {Promise>} a promise + * for an object containing key-value pairs, where keys + * are string identifiers for domain objects, and + * values are the corresponding domain objects themselves. + * @memberof module:core/object/object-provider.ObjectProvider# + */ + getObjects: getObjects + }; + } + + return DomainObjectProvider; + } +); \ No newline at end of file diff --git a/platform/core/src/types/MergeModels.js b/platform/core/src/types/MergeModels.js new file mode 100644 index 0000000000..f6b706ce54 --- /dev/null +++ b/platform/core/src/types/MergeModels.js @@ -0,0 +1,50 @@ +/*global define*/ + +/** + * Defines MergedModel, which allows a deep merge of domain object + * models, or JSONifiable JavaScript objects generally. + * + * @module core/model/merged-model + */ +define( + function () { + 'use strict'; + + function mergeModels(a, b, merger) { + var mergeFunction; + + function mergeArrays(a, b) { + return a.concat(b); + } + + function mergeObjects(a, b) { + var result = {}; + Object.keys(a).forEach(function (k) { + result[k] = b.hasOwnProperty(k) ? + mergeModels(a[k], b[k], (merger || {})[k]) : + a[k]; + }); + Object.keys(b).forEach(function (k) { + // Copy any properties not already merged + if (!a.hasOwnProperty(k)) { + result[k] = b[k]; + } + }); + return result; + } + + function mergeOther(a, b) { + return b; + } + + mergeFunction = merger && Function.isFunction(merger) ? merger : + (Array.isArray(a) && Array.isArray(b)) ? mergeArrays : + (a instanceof Object && b instanceof Object) ? mergeObjects : + mergeOther; + + return mergeFunction(a, b); + } + + return mergeModels; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypeCapability.js b/platform/core/src/types/TypeCapability.js new file mode 100644 index 0000000000..b507303a9d --- /dev/null +++ b/platform/core/src/types/TypeCapability.js @@ -0,0 +1,27 @@ +/*global define,Promise*/ + +/** + * Module defining TypeCapability. Created by vwoeltje on 11/10/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function TypeCapability(typeService, domainObject) { + var typeKey = domainObject.getModel().type, + type = typeService.getType(typeKey), + self = Object.create(type); + + self.invoke = function () { return self; }; + + return self; + } + + return TypeCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypeCapabilityProvider.js b/platform/core/src/types/TypeCapabilityProvider.js new file mode 100644 index 0000000000..4cc606d7d0 --- /dev/null +++ b/platform/core/src/types/TypeCapabilityProvider.js @@ -0,0 +1,113 @@ +/*global define,Promise*/ + +/** + * Type capability provider. Provides capabilities for domain objects based + * on properties defined as part of their type, where type is inferred from + * a string with key "type" in the model. + * + * These capabilities include the "type" capability, which providers + * information about an object's type not stored in the model. + * + * @module core/type/type-capability-provider + */ +define( + [], + function () { + 'use strict'; + var promises = { + merge: Promise.all, + decorate: function (promise, callback) { + return promise.then(callback); + }, + as: function (value) { + return Promise.resolve(value); + } + }; + + /** + * Construct a new type capability provider. The provided type + * service will be used to obtain type information; the provided + * module service will be used to load modules which define + * factories for capabilities associated with types. + * + * @param {TypeService} typeService the service which will + * provide type information/definitions + * @param {ModuleService} moduleService the service which + * shall allow other JavaScript modules to be loaded. + * @constructor + * @memberof module:core/type/type-capability-provider + */ + function TypeCapabilityProvider(typeService, moduleService) { + var typeFactories = {}; + + function buildTypeFactory(type) { + var capabilities = type.getDefinition().capabilities || []; + + return promises.decorate( + promises.merge(capabilities.map(moduleService.get)), + function (modules) { + var factory = {}; + + modules.filter(function (mod) { + return mod; + }).forEach(function (mod) { + var capFactories = mod.capabilities || {}; + Object.keys(capFactories).forEach(function (k) { + factory[k] = capFactories[k]; + }); + }); + + // And the "type" capability + factory.type = function () { + return type; + }; + + return factory; + } + ); + } + + /** + * Description (TODO) + * @param {string} typeKey Description + * @returns {string} Description TODO + */ + function promiseTypeFactory(typeKey) { + return (typeFactories[typeKey] = typeFactories[typeKey] || promises.decorate( + typeService.getType(typeKey), + buildTypeFactory + )); + } + + return { + /** + * Get a capability which should be expressed by a domain + * object. This will return a promise for the capability instance; + * if the named capaability is not provided for that domain + * object by this service, the result of the fulfilled promise + * will be undefined. + * + * @param {module:core/api/domain-object.DomainObject} domainObject the + * domain object which may or may not express this capability + * @param {string} name the name of the capability requested + * @memberof module:core/type/type-capability-provider.TypeCapabilityProvider# + */ + getCapability: function (domainObject, name) { + var typeKey = domainObject.getModel().type; + return promises.decorate( + promiseTypeFactory(typeKey), + function (typeFactory) { + return typeFactory[name] ? + typeFactory[name](domainObject) : + undefined; + } + ); + } + }; + } + + return { + instantiate: TypeCapabilityProvider + }; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypeImpl.js b/platform/core/src/types/TypeImpl.js new file mode 100644 index 0000000000..b65f0716fd --- /dev/null +++ b/platform/core/src/types/TypeImpl.js @@ -0,0 +1,153 @@ +/*global define*/ + +/** + * Type implementation. Defines a type object which wraps a + * type definition and exposes useful methods for inspecting + * that type and understanding its relationship to other + * types. + * + * @module core/type/type-impl + */ +define( + ['./TypeProperty'], + function (TypeProperty) { + "use strict"; + + /** + * Construct a new type. Types describe categories of + * domain objects. + * + * @param {TypeDefinition} typeDef an object containing + * key-value pairs describing a type and its + * relationship to other types. + * @memberof module:core/type/type-impl + */ + function TypeImpl(typeDef) { + var inheritList = typeDef.inherits || [], + featureSet = {}; + + (typeDef.features || []).forEach(function (feature) { + featureSet[feature] = true; + }); + + return { + /** + * Get the string key which identifies this type. + * This is the type's machine-readable name/identifier, + * and will correspond to the "type" field of the models + * of domain objects of this type. + * + * @returns {string} the key which identifies this type + * @memberof module:core/type/type-impl.TypeImpl# + */ + getKey: function () { + return typeDef.key; + }, + /** + * Get the human-readable name for this type, as should + * be displayed in the user interface when referencing + * this type. + * + * @returns {string} the human-readable name of this type + * @memberof module:core/type/type-impl.TypeImpl# + */ + getName: function () { + return typeDef.name; + }, + /** + * Get the human-readable description for this type, as should + * be displayed in the user interface when describing + * this type. + * + * @returns {string} the human-readable description of this type + * @memberof module:core/type/type-impl.TypeImpl# + */ + getDescription: function () { + return typeDef.description; + }, + /** + * Get the glyph associated with this type. Glyphs are + * single-character strings which will appear as icons (when + * displayed in an appropriate font) which visually + * distinguish types from one another. + * + * @returns {string} the glyph to be displayed + * @memberof module:core/type/type-impl.TypeImpl# + */ + getGlyph: function () { + return typeDef.glyph; + }, + /** + * Get an array of properties associated with objects of + * this type, as might be shown in a Create wizard or + * an Edit Properties view. + * + * @return {Array} properties associated with + * objects of this type + */ + getProperties: function () { + return (typeDef.properties || []).map(TypeProperty); + }, + /** + * Get the initial state of a model for domain objects of + * this type. + * + * @return {object} initial domain object model + */ + getInitialModel: function () { + return typeDef.model || {}; + }, + /** + * Get the raw type definition for this type. This is an + * object containing key-value pairs of type metadata; + * this allows the retrieval and use of custom type + * properties which are not recognized within this interface. + * + * @returns {object} the raw definition for this type + * @memberof module:core/type/type-impl.TypeImpl# + */ + getDefinition: function () { + return typeDef; + }, + /** + * Check if this type is or inherits from some other type. + * + * TODO: Rename, "instanceOf" is a misnomer (since there is + * no "instance", so to speak.) + * + * @param {string|module:core/type/type-implTypeImpl} key either + * a string key for a type, or an instance of a type + * object, which this + * @returns {boolean} true + * @memberof module:core/type/type-impl.TypeImpl# + */ + instanceOf: function instanceOf(key) { + + if (key === typeDef.key) { + return true; + } else if (inheritList.indexOf(key) > -1) { + return true; + } else if (!key) { + return true; + } else if (key !== null && typeof key === 'object') { + return key.getKey ? instanceOf(key.getKey()) : false; + } else { + return false; + } + }, + /** + * Check if a type should support a given feature. This simply + * checks for the presence or absence of the feature key in + * the type definition's "feature" field. + * @param {string} feature a string identifying the feature + * @returns {boolean} true if the feature is supported + */ + hasFeature: function (feature) { + return featureSet[feature] || false; + } + }; + } + + return TypeImpl; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypeProperty.js b/platform/core/src/types/TypeProperty.js new file mode 100644 index 0000000000..8bd12fc42f --- /dev/null +++ b/platform/core/src/types/TypeProperty.js @@ -0,0 +1,117 @@ +/*global define*/ + +/** + * Type property. Defines a mutable or displayable property + * associated with objects of a given type. + * + * @module core/type/type-property + */ +define( + ['./TypePropertyConversion'], + function (TypePropertyConversion) { + 'use strict'; + + /** + * Instantiate a property associated with domain objects of a + * given type. This provides an interface by which + * + * @constructor + * @memberof module:core/type/type-property + */ + function TypeProperty(propertyDefinition) { + // Load an appropriate conversion + var conversion = new TypePropertyConversion( + propertyDefinition.conversion || "identity" + ); + + // Perform a lookup for a value from an object, + // which may recursively look at contained objects + // based on the path provided. + function lookupValue(object, propertyPath) { + var value; + + // Can't look up from a non-object + if (!object) { + return undefined; + } + + // If path is not an array, just look up the property + if (!Array.isArray(propertyPath)) { + return object[propertyPath]; + } + + // Otherwise, look up in the sequence defined in the array + if (propertyPath.length > 0) { + value = object[propertyPath[0]]; + return propertyPath.length > 1 ? + lookupValue(value, propertyPath.slice(1)) : + value; + } else { + return undefined; + } + } + + function specifyValue(object, propertyPath, value) { + + // If path is not an array, just set the property + if (!Array.isArray(propertyPath)) { + object[propertyPath] = value; + } else if (propertyPath.length > 1) { + // Otherwise, look up in defined sequence + object[propertyPath[0]] = object[propertyPath[0]] || {}; + specifyValue( + object[propertyPath[0]], + propertyPath.slice(1), + value + ); + } else if (propertyPath.length === 1) { + object[propertyPath[0]] = value; + } + + } + + return { + /** + * Retrieve the value associated with this property + * from a given model. + */ + getValue: function (model) { + var property = propertyDefinition.property || + propertyDefinition.key; + + if (property) { + return conversion.toFormValue( + lookupValue(model, property) + ); + } else { + return undefined; + } + }, + /** + * Set a value associated with this property in + * an object's model. + */ + setValue: function setValue(model, value) { + var property = propertyDefinition.property || + propertyDefinition.key; + + value = conversion.toModelValue(value); + + if (property) { + return specifyValue(model, property, value); + } else { + return undefined; + } + }, + /** + * Get the raw definition for this property. + */ + getDefinition: function () { + return propertyDefinition; + } + }; + } + + return TypeProperty; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypePropertyConversion.js b/platform/core/src/types/TypePropertyConversion.js new file mode 100644 index 0000000000..a944682219 --- /dev/null +++ b/platform/core/src/types/TypePropertyConversion.js @@ -0,0 +1,64 @@ +/*global define*/ + +/** + * Defines type property conversions, used to convert values from + * a domain object model to values displayable in a form, and + * vice versa. + * @module core/type/type-property-conversion + */ +define( + function () { + 'use strict'; + + + var conversions = { + number: { + toModelValue: parseFloat, + toFormValue: function (modelValue) { + return (typeof modelValue === 'number') ? + modelValue.toString(10) : undefined; + } + }, + identity: { + toModelValue: function (v) { return v; }, + toFormValue: function (v) { return v; } + } + }, + ARRAY_SUFFIX = '[]'; + + // Utility function to handle arrays of conversiions + function ArrayConversion(conversion) { + return { + toModelValue: function (formValue) { + return formValue && formValue.map(conversion.toModelValue); + }, + toFormValue: function (modelValue) { + return modelValue && modelValue.map(conversion.toFormValue); + } + }; + } + + /** + * Look up an appropriate conversion between form values and model + * values, e.g. to numeric values. + */ + function TypePropertyConversion(name) { + if (name && + name.length > ARRAY_SUFFIX.length && + name.indexOf(ARRAY_SUFFIX, name.length - ARRAY_SUFFIX.length) !== -1) { + return new ArrayConversion( + new TypePropertyConversion( + name.substring(0, name.length - ARRAY_SUFFIX.length) + ) + ); + } else { + if (!conversions[name]) { + throw new Error("Unknown conversion type: " + name); + } + return conversions[name]; + } + } + + return TypePropertyConversion; + } +); \ No newline at end of file diff --git a/platform/core/src/types/TypeProvider.js b/platform/core/src/types/TypeProvider.js new file mode 100644 index 0000000000..14b0b6f2cf --- /dev/null +++ b/platform/core/src/types/TypeProvider.js @@ -0,0 +1,185 @@ +/*global define, Promise*/ + +/** + * Provides information about types of domain objects within the running + * Open MCT Web instance. + * + * @module core/type/type-provider + */ +define( + ['./TypeImpl', './MergeModels'], + function (TypeImpl, mergeModels) { + 'use strict'; + + var promises = { + merge: Promise.all, + decorate: function (promise, callback) { + return promise.then(callback); + }, + as: function (value) {return Promise.resolve(value); } + }, + TO_CONCAT = ['inherits', 'capabilities', 'properties', 'features'], + TO_MERGE = ['model']; + + function copyKeys(a, b) { + Object.keys(b).forEach(function (k) { + a[k] = b[k]; + }); + } + + function removeDuplicates(array) { + var set = {}; + return array ? array.filter(function (element) { + // Don't filter objects (e.g. property definitions) + if (element instanceof Object && !(element instanceof String)) { + return true; + } + + return set[element] ? + false : + (set[element] = true); + }) : array; + } + + /** + * Instantiate a new type provider. + * + * @param {Array} options.definitions the raw type + * definitions for this type. + * @constructor + * @memberof module:core/type/type-provider + */ + function TypeProvider(types) { + var rawTypeDefinitions = types, + typeDefinitions = (function (typeDefArray) { + var result = {}; + typeDefArray.forEach(function (typeDef) { + var k = typeDef.key; + if (k) { + result[k] = (result[k] || []).concat(typeDef); + } + }); + return result; + }(rawTypeDefinitions)), + typeMap = {}, + undefinedType; + + // Reduce an array of type definitions to a single type definiton, + // which has merged all properties in order. + function collapse(typeDefs) { + var collapsed = typeDefs.reduce(function (a, b) { + var result = {}; + copyKeys(result, a); + copyKeys(result, b); + + // Special case: Do a merge, e.g. on "model" + TO_MERGE.forEach(function (k) { + if (a[k] && b[k]) { + result[k] = mergeModels(a[k], b[k]); + } + }); + + // Special case: Concatenate certain arrays + TO_CONCAT.forEach(function (k) { + if (a[k] || b[k]) { + result[k] = (a[k] || []).concat(b[k] || []); + } + }); + return result; + }, {}); + + // Remove any duplicates from the collapsed array + TO_CONCAT.forEach(function (k) { + if (collapsed[k]) { + collapsed[k] = removeDuplicates(collapsed[k]); + } + }); + return collapsed; + } + + function getUndefinedType() { + return (undefinedType = undefinedType || collapse( + rawTypeDefinitions.filter(function (typeDef) { + return !typeDef.key; + }) + )); + } + + function asArray(value) { + return Array.isArray(value) ? value : [value]; + } + + function lookupTypeDef(typeKey) { + function buildTypeDef(typeKey) { + var typeDefs = typeDefinitions[typeKey] || [], + inherits = typeDefs.map(function (typeDef) { + return asArray(typeDef.inherits || []); + }).reduce(function (a, b) { + return a.concat(b); + }, []), + def = collapse( + [getUndefinedType()].concat( + inherits.map(lookupTypeDef) + ).concat(typeDefs) + ); + + // Always provide a default name + def.model = def.model || {}; + def.model.name = def.model.name || ( + "Unnamed " + (def.name || "Object") + ); + + return def; + + } + + return (typeMap[typeKey] = typeMap[typeKey] || buildTypeDef(typeKey)); + } + + + return { + /** + * Get a list of all types defined by this service. + * + * @returns {Promise>} a + * promise for an array of all type instances defined + * by this service. + * @memberof module:core/type/type-provider.TypeProvider# + */ + listTypes: function () { + var self = this; + return removeDuplicates( + rawTypeDefinitions.filter(function (def) { + return def.key; + }).map(function (def) { + return def.key; + }).map(function (key) { + return self.getType(key); + }) + ); + }, + + /** + * Get a specific type by name. + * + * @param {string} key the key (machine-readable identifier) + * for the type of interest + * @returns {Promise} a + * promise for a type object identified by this key. + * @memberof module:core/type/type-provider.TypeProvider# + */ + getType: function (key) { + return new TypeImpl(lookupTypeDef(key)); + } + }; + } + + // Services framework is designed to expect factories + TypeProvider.instantiate = TypeProvider; + + return TypeProvider; + + + } + +); \ No newline at end of file diff --git a/platform/core/src/types/TypeWizard.js b/platform/core/src/types/TypeWizard.js new file mode 100644 index 0000000000..8953f48645 --- /dev/null +++ b/platform/core/src/types/TypeWizard.js @@ -0,0 +1,70 @@ +/*global define*/ + +/** + * Default type wizard. Type wizards provide both a declarative + * description of the form which should be presented to a user + * when a new domain object of a given type is instantiatd, as + * well as the necessary functionality to convert this user input + * to a model for a domain object. + * + * This wizard is intended to be both a general-purpose default + * and a useful supertype; custom wizards for types which + * require additional information should use this as a base + * and append to the results of getSections(..) or add + * properties to the result of createModel(..) as appropriate + * to the more specific type. + * + * @module core/type/type-wizard + */ +define( + { + /** + * Get all sections appropriate to the display of this type. + * + * @returns {Array} + * @method + */ + getSections: function (typeKey) { + 'use strict'; + + return [ + { + label: "Title Options", + rows: [ + { + control: '_textfield', + label: "Title", + key: "name" + }, + { + control: '_checkbox', + label: "Display title by default", + key: "displayTitle" + } + ] + } + ]; + }, + + /** + * Create a model for a domain object based on user input. + * + * @param {object} formValue an object containing key-value + * pairs, where keys are properties indicated as part + * of a form's definition, and values are the result + * of user input. + * @return {object} the new model for a domain object + */ + createModel: function (formValue) { + 'use strict'; + + var model = { + type: formValue.type, + name: formValue.name, + display: { title: formValue.displayTitle } + }; + + return model; + } + } +); \ No newline at end of file diff --git a/platform/core/src/views/ViewCapability.js b/platform/core/src/views/ViewCapability.js new file mode 100644 index 0000000000..40eaf7adb7 --- /dev/null +++ b/platform/core/src/views/ViewCapability.js @@ -0,0 +1,25 @@ +/*global define,Promise*/ + +/** + * Module defining ViewCapability. Created by vwoeltje on 11/10/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ViewCapability(viewService, domainObject) { + return { + invoke: function () { + return viewService.getViews(domainObject); + } + }; + } + + return ViewCapability; + } +); \ No newline at end of file diff --git a/platform/core/src/views/ViewProvider.js b/platform/core/src/views/ViewProvider.js new file mode 100644 index 0000000000..c60e46d9c5 --- /dev/null +++ b/platform/core/src/views/ViewProvider.js @@ -0,0 +1,55 @@ +/*global define,Promise*/ + +/** + * Module defining ViewProvider. Created by vwoeltje on 11/10/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function ViewProvider(views) { + + function capabilitiesMatch(domainObject, capabilities, allowDelegation) { + var delegation = domainObject.getCapability("delegation"); + + allowDelegation = allowDelegation && (delegation !== undefined); + + function hasCapability(c) { + return domainObject.hasCapability(c) || + (allowDelegation && delegation.doesDelegateCapability(c)); + } + + function and(a, b) { + return a && b; + } + + return capabilities.map(hasCapability).reduce(and, true); + } + + function getViews(domainObject) { + var type = domainObject.useCapability("type"); + + return views.filter(function (view) { + return (!view.type) || type.instanceOf(view.type); + }).filter(function (view) { + return capabilitiesMatch( + domainObject, + view.needs || [], + view.delegation || false + ); + }); + } + + return { + getViews: getViews + }; + } + + return ViewProvider; + } +); \ No newline at end of file diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json new file mode 100644 index 0000000000..671c5baf6f --- /dev/null +++ b/platform/core/test/suite.json @@ -0,0 +1,6 @@ +[ + "types/TypeImpl", + "types/TypeProperty", + "types/TypePropertyConversion", + "types/TypeProvider" +] \ No newline at end of file diff --git a/platform/core/test/types/TypeCapabilityProviderSpec.js b/platform/core/test/types/TypeCapabilityProviderSpec.js new file mode 100644 index 0000000000..a351676657 --- /dev/null +++ b/platform/core/test/types/TypeCapabilityProviderSpec.js @@ -0,0 +1,13 @@ +/*global define,describe,it,beforeEach,expect*/ + +define( + ['../../src/types/TypeCapabilityProvider'], + function () { + + 'use strict'; + + describe("Type capability provider", function () { + }); + + } +); \ No newline at end of file diff --git a/platform/core/test/types/TypeImplSpec.js b/platform/core/test/types/TypeImplSpec.js new file mode 100644 index 0000000000..ea0c846bb3 --- /dev/null +++ b/platform/core/test/types/TypeImplSpec.js @@ -0,0 +1,72 @@ +/*global define,describe,it,expect,beforeEach*/ + +define( + ['../../src/types/TypeImpl'], + function (typeImpl) { + "use strict"; + + describe("Type definition wrapper", function () { + var testTypeDef, + type; + + beforeEach(function () { + testTypeDef = { + key: 'test-type', + name: 'Test Type', + description: 'A type, for testing', + glyph: 't', + inherits: ['test-parent-1', 'test-parent-2'] + }; + type = typeImpl(testTypeDef); + }); + + it("exposes key from definition", function () { + expect(type.getKey()).toEqual('test-type'); + }); + + it("exposes name from definition", function () { + expect(type.getName()).toEqual('Test Type'); + }); + + it("exposes description from definition", function () { + expect(type.getDescription()).toEqual('A type, for testing'); + }); + + it("exposes glyph from definition", function () { + expect(type.getGlyph()).toEqual('t'); + }); + + it("exposes its underlying type definition", function () { + expect(type.getDefinition()).toEqual(testTypeDef); + }); + + it("supports instance-of checks by type key", function () { + expect(type.instanceOf('test-parent-1')).toBeTruthy(); + expect(type.instanceOf('test-parent-2')).toBeTruthy(); + expect(type.instanceOf('some-other-type')).toBeFalsy(); + }); + + it("supports instance-of checks by specific type key", function () { + expect(type.instanceOf('test-type')).toBeTruthy(); + }); + + it("supports instance-of checks by type object", function () { + expect(type.instanceOf({ + getKey: function () { return 'test-parent-1'; } + })).toBeTruthy(); + expect(type.instanceOf({ + getKey: function () { return 'some-other-type'; } + })).toBeFalsy(); + }); + + it("correctly recognizes instance-of checks upon itself", function () { + expect(type.instanceOf(type)).toBeTruthy(); + }); + + it("recognizes that all types are instances of the undefined type", function () { + expect(type.instanceOf()).toBeTruthy(); + expect(type.instanceOf({ getKey: function () {} })).toBeTruthy(); + }); + }); + } +); \ No newline at end of file diff --git a/platform/core/test/types/TypePropertyConversionSpec.js b/platform/core/test/types/TypePropertyConversionSpec.js new file mode 100644 index 0000000000..f4f31820bd --- /dev/null +++ b/platform/core/test/types/TypePropertyConversionSpec.js @@ -0,0 +1,45 @@ +/*global define,describe,it,xit,expect,beforeEach*/ + +define( + ['../../src/types/TypePropertyConversion'], + function (TypePropertyConversion) { + "use strict"; + + describe("Type property conversion", function () { + + it("allows non-conversion when parameter is 'identity'", function () { + var conversion = new TypePropertyConversion("identity"); + [ 42, "42", { a: 42 } ].forEach(function (v) { + expect(conversion.toFormValue(v)).toBe(v); + expect(conversion.toModelValue(v)).toBe(v); + }); + }); + + it("allows numeric conversion", function () { + var conversion = new TypePropertyConversion("number"); + expect(conversion.toFormValue(42)).toBe("42"); + expect(conversion.toModelValue("42")).toBe(42); + }); + + it("supports array conversions", function () { + var conversion = new TypePropertyConversion("number[]"); + expect(conversion.toFormValue([42, 44]).length).toEqual(2); + expect(conversion.toFormValue([42, 44])[0]).toBe("42"); + expect(conversion.toModelValue(["11", "42"])[1]).toBe(42); + }); + + it("throws exceptions on unrecognized conversions", function () { + var caught = false, tmp; + + try { + tmp = new TypePropertyConversion("some-unknown-conversion"); + } catch (e) { + caught = true; + } + + expect(caught).toBeTruthy(); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/core/test/types/TypePropertySpec.js b/platform/core/test/types/TypePropertySpec.js new file mode 100644 index 0000000000..a1345b7192 --- /dev/null +++ b/platform/core/test/types/TypePropertySpec.js @@ -0,0 +1,61 @@ +/*global define,describe,it,xit,expect,beforeEach*/ + +define( + ['../../src/types/TypeProperty'], + function (TypeProperty) { + "use strict"; + + describe("Type property", function () { + + it("allows retrieval of its definition", function () { + var definition = { key: "hello", someOtherKey: "hm?" }; + expect( + new TypeProperty(definition).getDefinition() + ).toEqual(definition); + }); + + it("sets properties in object models", function () { + var definition = { + key: "someKey", + property: "someProperty" + }, + model = {}, + property = new TypeProperty(definition); + property.setValue(model, "some value"); + expect(model.someProperty).toEqual("some value"); + }); + + it("gets properties from object models", function () { + var definition = { + key: "someKey", + property: "someProperty" + }, + model = { someProperty: "some value"}, + property = new TypeProperty(definition); + expect(property.getValue(model)).toEqual("some value"); + }); + + it("sets properties by path", function () { + var definition = { + key: "someKey", + property: [ "some", "property" ] + }, + model = {}, + property = new TypeProperty(definition); + property.setValue(model, "some value"); + expect(model.some.property).toEqual("some value"); + }); + + it("gets properties by path", function () { + var definition = { + key: "someKey", + property: [ "some", "property" ] + }, + model = { some: { property: "some value" } }, + property = new TypeProperty(definition); + expect(property.getValue(model)).toEqual("some value"); + }); + + }); + } +); \ No newline at end of file diff --git a/platform/core/test/types/TypeProviderSpec.js b/platform/core/test/types/TypeProviderSpec.js new file mode 100644 index 0000000000..500f611a9c --- /dev/null +++ b/platform/core/test/types/TypeProviderSpec.js @@ -0,0 +1,214 @@ +/*global define,describe,it,expect,beforeEach, waitsFor, runs*/ + +define( + ['../../src/types/TypeProvider'], + function (typeProviderModule) { + "use strict"; + + describe("Type provider", function () { + + var captured = {}, + capture = function (name) { + return function (value) { + captured[name] = value; + }; + }, + testTypeDefinitions = [ + { + key: 'basic', + glyph: "X", + name: "Basic Type" + }, + { + key: 'multi1', + glyph: "Z", + description: "Multi1 Description", + capabilities: ['a1', 'b1'] + }, + { + key: 'multi2', + glyph: "Y", + capabilities: ['a2', 'b2', 'c2'] + }, + { + key: 'single-subtype', + inherits: 'basic', + name: "Basic Subtype", + description: "A test subtype" + }, + { + key: 'multi-subtype', + inherits: ['multi1', 'multi2'], + name: "Multi-parent Subtype", + capabilities: ['a3'] + }, + { + name: "Default" + } + ], + provider; + + beforeEach(function () { + captured = {}; + provider = typeProviderModule.instantiate({ + definitions: testTypeDefinitions + }); + }); + + it("can be instantiated from a factory method", function () { + expect(provider).toBeTruthy(); + }); + + it("looks up non-inherited types by name", function () { + provider.getType('basic').then(capture('type')); + + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getGlyph()).toEqual("X"); + expect(captured.type.getName()).toEqual("Basic Type"); + expect(captured.type.getDescription()).toBeUndefined(); + }); + }); + + it("supports single inheritance", function () { + provider.getType('single-subtype').then(capture('type')); + + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getGlyph()).toEqual("X"); + expect(captured.type.getName()).toEqual("Basic Subtype"); + expect(captured.type.getDescription()).toEqual("A test subtype"); + }); + }); + + it("supports multiple inheritance", function () { + provider.getType('multi-subtype').then(capture('type')); + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getGlyph()).toEqual("Y"); + expect(captured.type.getName()).toEqual("Multi-parent Subtype"); + expect(captured.type.getDescription()).toEqual("Multi1 Description"); + }); + }); + + it("concatenates capabilities in order", function () { + provider.getType('multi-subtype').then(capture('type')); + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getDefinition().capabilities).toEqual( + ['a1', 'b1', 'a2', 'b2', 'c2', 'a3'] + ); + }); + }); + + it("allows lookup of the undefined type", function () { + provider.getType(undefined).then(capture('type')); + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getName()).toEqual("Default"); + }); + }); + + it("concatenates capabilities of all undefined types", function () { + typeProviderModule.instantiate({ + definitions: testTypeDefinitions.concat([ + { + capabilities: ['a', 'b', 'c'] + }, + { + capabilities: ['x', 'y', 'z'] + } + ]) + }).getType(undefined).then(capture('type')); + + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getDefinition().capabilities).toEqual( + ['a', 'b', 'c', 'x', 'y', 'z'] + ); + }); + }); + + it("includes capabilities from undefined type in all types", function () { + typeProviderModule.instantiate({ + definitions: testTypeDefinitions.concat([ + { + capabilities: ['a', 'b', 'c'] + }, + { + capabilities: ['x', 'y', 'z'] + } + ]) + }).getType('multi-subtype').then(capture('type')); + waitsFor( + function () { + return captured.type !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.type.getDefinition().capabilities).toEqual( + ['a', 'b', 'c', 'x', 'y', 'z', 'a1', 'b1', 'a2', 'b2', 'c2', 'a3'] + ); + }); + }); + + it("allows types to be listed", function () { + provider.listTypes().then(capture('types')); + waitsFor( + function () { + return captured.types !== undefined; + }, + "promise resolution", + 250 + ); + runs(function () { + expect(captured.types.length).toEqual( + testTypeDefinitions.filter(function (t) { + return t.key; + }).length + ); + }); + }); + + + }); + } +); \ No newline at end of file