[Code Style] Use prototypes in framework layer

WTD-1482
This commit is contained in:
Victor Woeltjen 2015-08-14 14:42:25 -07:00
parent 7fe866060b
commit edca2a9f03
14 changed files with 750 additions and 676 deletions

View File

@ -40,24 +40,36 @@ define(
* *
* @memberof platform/framework * @memberof platform/framework
* @constructor * @constructor
* @param {BundleLoader} loader * @param {platform/framework.BundleLoader} loader
* @param {BundleResolver} resolver * @param {platform/framework.BundleResolver} resolver
* @param {ExtensionRegistrar} registrar * @param {platform/framework.ExtensionRegistrar} registrar
* @param {ApplicationBootstrapper} bootstrapper * @param {platform/framework.ApplicationBootstrapper} bootstrapper
*/ */
function FrameworkInitializer(loader, resolver, registrar, bootstrapper) { function FrameworkInitializer(loader, resolver, registrar, bootstrapper) {
this.loader = loader;
this.resolver = resolver;
this.registrar = registrar;
this.bootstrapper = bootstrapper;
}
return { function bind(method, thisArg) {
runApplication: function (bundleList) { return function () {
return loader.loadBundles(bundleList) return method.apply(thisArg, arguments);
.then(resolver.resolveBundles)
.then(registrar.registerExtensions)
.then(bootstrapper.bootstrap);
}
}; };
} }
/**
* Run the application defined by this set of bundles.
* @param bundleList
* @returns {*}
*/
FrameworkInitializer.prototype.runApplication = function (bundleList) {
return this.loader.loadBundles(bundleList)
.then(bind(this.resolver.resolveBundles, this.resolver))
.then(bind(this.registrar.registerExtensions, this.registrar))
.then(bind(this.bootstrapper.bootstrap, this.bootstrapper));
};
return FrameworkInitializer; return FrameworkInitializer;
} }
); );

View File

@ -53,7 +53,29 @@ define(
*/ */
function LogLevel(level) { function LogLevel(level) {
// Find the numeric level associated with the string // Find the numeric level associated with the string
var index = LOG_LEVELS.indexOf(level); this.index = LOG_LEVELS.indexOf(level);
// Default to 'warn' level if unspecified
if (this.index < 0) {
this.index = 1;
}
}
/**
* Configure logging to suppress log output if it is
* not of an appropriate level. Both the Angular app
* being initialized and a reference to `$log` should be
* passed; the former is used to configure application
* logging, while the latter is needed to apply the
* same configuration during framework initialization
* (since the framework also logs.)
*
* @param app the Angular app to configure
* @param $log Angular's $log (also configured)
* @memberof platform/framework.LogLevel#
*/
LogLevel.prototype.configure = function (app, $log) {
var index = this.index;
// Replace logging methods with no-ops, if they are // Replace logging methods with no-ops, if they are
// not of an appropriate level. // not of an appropriate level.
@ -67,36 +89,14 @@ define(
}); });
} }
// Default to 'warn' level if unspecified decorate($log);
if (index < 0) { app.config(function ($provide) {
index = 1; $provide.decorator('$log', function ($delegate) {
} decorate($delegate);
return $delegate;
return { });
/** });
* Configure logging to suppress log output if it is };
* not of an appropriate level. Both the Angular app
* being initialized and a reference to `$log` should be
* passed; the former is used to configure application
* logging, while the latter is needed to apply the
* same configuration during framework initialization
* (since the framework also logs.)
*
* @param app the Angular app to configure
* @param $log Angular's $log (also configured)
* @memberof platform/framework.LogLevel#
*/
configure: function (app, $log) {
decorate($log);
app.config(function ($provide) {
$provide.decorator('$log', function ($delegate) {
decorate($delegate);
return $delegate;
});
});
}
};
}
return LogLevel; return LogLevel;
} }

View File

@ -42,25 +42,27 @@ define(
* @constructor * @constructor
*/ */
function ApplicationBootstrapper(angular, document, $log) { function ApplicationBootstrapper(angular, document, $log) {
return { this.angular = angular;
/** this.document = document;
* Bootstrap the application. this.$log = $log;
*
* @method
* @memberof ApplicationBootstrapper#
* @param {angular.Module} app the Angular application to
* bootstrap
* @memberof platform/framework.ApplicationBootstrapper#
*/
bootstrap: function (app) {
$log.info("Bootstrapping application " + (app || {}).name);
angular.element(document).ready(function () {
angular.bootstrap(document, [app.name]);
});
}
};
} }
/**
* Bootstrap the application.
*
* @param {angular.Module} app the Angular application to
* bootstrap
*/
ApplicationBootstrapper.prototype.bootstrap = function (app) {
var angular = this.angular,
document = this.document,
$log = this.$log;
$log.info("Bootstrapping application " + (app || {}).name);
angular.element(document).ready(function () {
angular.bootstrap(document, [app.name]);
});
};
return ApplicationBootstrapper; return ApplicationBootstrapper;
} }
); );

View File

@ -56,13 +56,7 @@ define(
function Bundle(path, bundleDefinition) { function Bundle(path, bundleDefinition) {
// Start with defaults // Start with defaults
var definition = Object.create(Constants.DEFAULT_BUNDLE), var definition = Object.create(Constants.DEFAULT_BUNDLE),
logName = path, logName = path;
self;
// Utility function for resolving paths in this bundle
function resolvePath(elements) {
return [path].concat(elements || []).join(Constants.SEPARATOR);
}
// Override defaults with specifics from bundle definition // Override defaults with specifics from bundle definition
Object.keys(bundleDefinition).forEach(function (k) { Object.keys(bundleDefinition).forEach(function (k) {
@ -81,135 +75,138 @@ define(
logName += ")"; logName += ")";
} }
self = { this.path = path;
/** this.definition = definition;
* Get the path to this bundle. this.logName = logName;
* @memberof Bundle#
* @returns {string}
* @memberof platform/framework.Bundle#
*/
getPath: function () {
return path;
},
/**
* Get the path to this bundle's source folder. If an
* argument is provided, the path will be to the source
* file within the bundle's source file.
*
* @memberof Bundle#
* @param {string} [sourceFile] optionally, give a path to
* a specific source file in the bundle.
* @returns {string}
* @memberof platform/framework.Bundle#
*/
getSourcePath: function (sourceFile) {
var subpath = sourceFile ?
[ definition.sources, sourceFile ] :
[ definition.sources ];
return resolvePath(subpath);
},
/**
* Get the path to this bundle's resource folder. If an
* argument is provided, the path will be to the resource
* file within the bundle's resource file.
*
* @memberof Bundle#
* @param {string} [resourceFile] optionally, give a path to
* a specific resource file in the bundle.
* @returns {string}
* @memberof platform/framework.Bundle#
*/
getResourcePath: function (resourceFile) {
var subpath = resourceFile ?
[ definition.resources, resourceFile ] :
[ definition.resources ];
return resolvePath(subpath);
},
/**
* Get the path to this bundle's library folder. If an
* argument is provided, the path will be to the library
* file within the bundle's resource file.
*
* @memberof Bundle#
* @param {string} [libraryFile] optionally, give a path to
* a specific library file in the bundle.
* @returns {string}
* @memberof platform/framework.Bundle#
*/
getLibraryPath: function (libraryFile) {
var subpath = libraryFile ?
[ definition.libraries, libraryFile ] :
[ definition.libraries ];
return resolvePath(subpath);
},
/**
* Get library configuration for this bundle. This is read
* from the bundle's definition; if the bundle is well-formed,
* it will resemble a require.config object.
* @memberof Bundle#
* @returns {object}
* @memberof platform/framework.Bundle#
*/
getConfiguration: function () {
return definition.configuration || {};
},
/**
* Get a log-friendly name for this bundle; this will
* include both the key (machine-readable name for this
* bundle) and the name (human-readable name for this
* bundle.)
* @returns {string} log-friendly name for this bundle
* @memberof platform/framework.Bundle#
*/
getLogName: function () {
return logName;
},
/**
* Get all extensions exposed by this bundle of a given
* category.
*
* @param category
* @memberof Bundle#
* @returns {Array}
* @memberof platform/framework.Bundle#
*/
getExtensions: function (category) {
var extensions = definition.extensions[category] || [];
return extensions.map(function objectify(extDefinition) {
return new Extension(self, category, extDefinition);
});
},
/**
* Get a list of all categories of extension exposed by
* this bundle.
*
* @memberof Bundle#
* @returns {Array}
* @memberof platform/framework.Bundle#
*/
getExtensionCategories: function () {
return Object.keys(definition.extensions);
},
/**
* Get the plain definition of this bundle, as read from
* its JSON declaration.
*
* @memberof Bundle#
* @returns {BundleDefinition} the raw definition of this bundle
* @memberof platform/framework.Bundle#
*/
getDefinition: function () {
return definition;
}
};
return self;
} }
// Utility function for resolving paths in this bundle
Bundle.prototype.resolvePath = function (elements) {
var path = this.path;
return [path].concat(elements || []).join(Constants.SEPARATOR);
};
/**
* Get the path to this bundle.
* @returns {string} path to this bundle;
*/
Bundle.prototype.getPath = function () {
return this.path;
};
/**
* Get the path to this bundle's source folder. If an
* argument is provided, the path will be to the source
* file within the bundle's source file.
*
* @param {string} [sourceFile] optionally, give a path to
* a specific source file in the bundle.
* @returns {string} path to the source folder (or to the
* source file within it)
*/
Bundle.prototype.getSourcePath = function (sourceFile) {
var subpath = sourceFile ?
[ this.definition.sources, sourceFile ] :
[ this.definition.sources ];
return this.resolvePath(subpath);
};
/**
* Get the path to this bundle's resource folder. If an
* argument is provided, the path will be to the resource
* file within the bundle's resource file.
*
* @param {string} [resourceFile] optionally, give a path to
* a specific resource file in the bundle.
* @returns {string} path to the resource folder (or to the
* resource file within it)
*/
Bundle.prototype.getResourcePath = function (resourceFile) {
var subpath = resourceFile ?
[ this.definition.resources, resourceFile ] :
[ this.definition.resources ];
return this.resolvePath(subpath);
};
/**
* Get the path to this bundle's library folder. If an
* argument is provided, the path will be to the library
* file within the bundle's resource file.
*
* @param {string} [libraryFile] optionally, give a path to
* a specific library file in the bundle.
* @returns {string} path to the resource folder (or to the
* resource file within it)
*/
Bundle.prototype.getLibraryPath = function (libraryFile) {
var subpath = libraryFile ?
[ this.definition.libraries, libraryFile ] :
[ this.definition.libraries ];
return this.resolvePath(subpath);
};
/**
* Get library configuration for this bundle. This is read
* from the bundle's definition; if the bundle is well-formed,
* it will resemble a require.config object.
* @returns {object} library configuration
*/
Bundle.prototype.getConfiguration = function () {
return this.definition.configuration || {};
};
/**
* Get a log-friendly name for this bundle; this will
* include both the key (machine-readable name for this
* bundle) and the name (human-readable name for this
* bundle.)
* @returns {string} log-friendly name for this bundle
*/
Bundle.prototype.getLogName = function () {
return this.logName;
};
/**
* Get all extensions exposed by this bundle of a given
* category.
*
* @param {string} category name of the extension category
* @returns {Array} extension definitions of that cataegory
*/
Bundle.prototype.getExtensions = function (category) {
var extensions = this.definition.extensions[category] || [],
self = this;
return extensions.map(function objectify(extDefinition) {
return new Extension(self, category, extDefinition);
});
};
/**
* Get a list of all extension categories exposed by this bundle.
*
* @returns {string[]} the extension categories
*/
Bundle.prototype.getExtensionCategories = function () {
return Object.keys(this.definition.extensions);
};
/**
* Get the plain definition of this bundle, as read from
* its JSON declaration.
*
* @returns {platform/framework.BundleDefinition} the raw
* definition of this bundle
*/
Bundle.prototype.getDefinition = function () {
return this.definition;
};
return Bundle; return Bundle;
} }
); );

View File

@ -41,10 +41,26 @@ define(
* *
* @memberof platform/framework * @memberof platform/framework
* @constructor * @constructor
* @param {object} $http Angular's HTTP requester * @param $http Angular's HTTP requester
* @param {object} $log Angular's logging service * @param $log Angular's logging service
*/ */
function BundleLoader($http, $log) { function BundleLoader($http, $log) {
this.$http = $http;
this.$log = $log;
}
/**
* Load a group of bundles, to be used to constitute the
* application by later framework initialization phases.
*
* @param {string|string[]} an array of bundle names to load, or
* the name of a JSON file containing that array
* @returns {Promise.<Bundle[]>} a promise for the loaded bundles
*/
BundleLoader.prototype.loadBundles = function (bundles) {
var $http = this.$http,
$log = this.$log;
// Utility function; load contents of JSON file using $http // Utility function; load contents of JSON file using $http
function getJSON(file) { function getJSON(file) {
@ -92,7 +108,7 @@ define(
var bundlePromises = bundleArray.map(loadBundle); var bundlePromises = bundleArray.map(loadBundle);
return Promise.all(bundlePromises) return Promise.all(bundlePromises)
.then(filterBundles); .then(filterBundles);
} }
// Load all bundles named in the referenced file. The file is // Load all bundles named in the referenced file. The file is
@ -101,31 +117,10 @@ define(
return getJSON(listFile).then(loadBundlesFromArray); return getJSON(listFile).then(loadBundlesFromArray);
} }
// Load all indicated bundles. If the argument is an array, return Array.isArray(bundles) ? loadBundlesFromArray(bundles) :
// this is taken to be a list of all bundles to load; if it (typeof bundles === 'string') ? loadBundlesFromFile(bundles) :
// is a string, then it is treated as the name of a JSON Promise.reject(new Error(INVALID_ARGUMENT_MESSAGE));
// file containing the list of bundles to load. };
function loadBundles(bundles) {
return Array.isArray(bundles) ? loadBundlesFromArray(bundles) :
(typeof bundles === 'string') ? loadBundlesFromFile(bundles) :
Promise.reject(new Error(INVALID_ARGUMENT_MESSAGE));
}
return {
/**
* Load a group of bundles, to be used to constitute the
* application by later framework initialization phases.
*
* @memberof BundleLoader#
* @param {string|string[]} an array of bundle names to load, or
* the name of a JSON file containing that array
* @returns {Promise.<Bundle[]>}
* @memberof platform/framework.BundleLoader#
*/
loadBundles: loadBundles
};
}
return BundleLoader; return BundleLoader;
} }

View File

@ -78,94 +78,93 @@ define(
// Attach bundle metadata // Attach bundle metadata
extensionDefinition.bundle = bundle.getDefinition(); extensionDefinition.bundle = bundle.getDefinition();
return { this.logName = logName;
/** this.bundle = bundle;
* Get the machine-readable identifier for this extension. this.category = category;
* this.definition = definition;
* @returns {string} this.extensionDefinition = extensionDefinition;
* @memberof platform/framework.Extension#
*/
getKey: function () {
return definition.key || "undefined";
},
/**
* Get the bundle which declared this extension.
*
* @memberof Extension#
* @returns {Bundle}
* @memberof platform/framework.Extension#
*/
getBundle: function () {
return bundle;
},
/**
* Get the category into which this extension falls.
* (e.g. "directives")
*
* @memberof Extension#
* @returns {string}
* @memberof platform/framework.Extension#
*/
getCategory: function () {
return category;
},
/**
* Check whether or not this extension should have an
* associated implementation module which may need to
* be loaded.
*
* @returns {boolean} true if an implementation separate
* from this definition should also be loaded
* @memberof platform/framework.Extension#
*/
hasImplementation: function () {
return definition.implementation !== undefined;
},
/**
* Get the path to the AMD module which implements this
* extension. Will return undefined if there is no
* implementation associated with this extension.
*
* @memberof Extension#
* @returns {string} path to implementation, or undefined
* @memberof platform/framework.Extension#
*/
getImplementationPath: function () {
return definition.implementation ?
bundle.getSourcePath(definition.implementation) :
undefined;
},
/**
* Get a log-friendly name for this extension; this will
* include both the key (machine-readable name for this
* extension) and the name (human-readable name for this
* extension.)
* @returns {string} log-friendly name for this extension
* @memberof platform/framework.Extension#
*/
getLogName: function () {
return logName;
},
/**
* Get the plain definition of the extension.
*
* Note that this definition will have an additional "bundle"
* field which points back to the bundle which defined the
* extension, as a convenience.
*
* @memberof Extension#
* @returns {ExtensionDefinition} the plain definition of
* this extension, as read from the bundle
* declaration.
* @memberof platform/framework.Extension#
*/
getDefinition: function () {
return extensionDefinition;
}
};
} }
/**
* Get the machine-readable identifier for this extension.
*
* @returns {string} the identifier for this extension
*/
Extension.prototype.getKey = function () {
return this.definition.key || "undefined";
};
/**
* Get the bundle which declared this extension.
*
* @returns {Bundle} the declaring bundle
*/
Extension.prototype.getBundle = function () {
return this.bundle;
};
/**
* Get the category into which this extension falls.
* (e.g. "directives")
*
* @returns {string} the extension category
*/
Extension.prototype.getCategory = function () {
return this.category;
};
/**
* Check whether or not this extension should have an
* associated implementation module which may need to
* be loaded.
*
* @returns {boolean} true if an implementation separate
* from this definition should also be loaded
*/
Extension.prototype.hasImplementation = function () {
return this.definition.implementation !== undefined;
};
/**
* Get the path to the AMD module which implements this
* extension. Will return undefined if there is no
* implementation associated with this extension.
*
* @returns {string} path to implementation, or undefined
*/
Extension.prototype.getImplementationPath = function () {
return this.definition.implementation ?
this.bundle.getSourcePath(this.definition.implementation) :
undefined;
};
/**
* Get a log-friendly name for this extension; this will
* include both the key (machine-readable name for this
* extension) and the name (human-readable name for this
* extension.)
*
* @returns {string} log-friendly name for this extension
*/
Extension.prototype.getLogName = function () {
return this.logName;
};
/**
* Get the plain definition of the extension.
*
* Note that this definition will have an additional "bundle"
* field which points back to the bundle which defined the
* extension, as a convenience.
*
* @returns {ExtensionDefinition} the plain definition of
* this extension, as read from the bundle
* declaration.
*/
Extension.prototype.getDefinition = function () {
return this.extensionDefinition;
};
return Extension; return Extension;
} }

View File

@ -37,131 +37,191 @@ define(
* @constructor * @constructor
*/ */
function CustomRegistrars(app, $log) { function CustomRegistrars(app, $log) {
this.app = app;
this.$log = $log;
}
// Used to create custom registration functions which map to // Utility; bind a function to a "this" pointer
// named methods on Angular modules, which follow the normal function bind(fn, thisArg) {
// app.method(key, [ deps..., function ]) pattern. return function () {
function CustomRegistrar(angularFunction) { return fn.apply(thisArg, arguments);
return function (extension, index) {
var key = extension.key,
dependencies = extension.depends || [];
if (!key) {
$log.warn([
"Cannot register ",
angularFunction,
" ",
index,
", no key specified. ",
JSON.stringify(extension)
].join(""));
} else {
$log.info([
"Registering ",
angularFunction,
": ",
key
].join(""));
app[angularFunction](
key,
dependencies.concat([extension])
);
}
};
}
function registerConstant(extension) {
var key = extension.key,
value = extension.value;
if (typeof key === "string" && value !== undefined) {
$log.info([
"Registering constant: ",
key,
" with value ",
value
].join(""));
app.constant(key, value);
} else {
$log.warn([
"Cannot register constant ",
key,
" with value ",
value
].join(""));
}
}
// Custom registration function for extensions of category "runs"
function registerRun(extension) {
if (typeof extension === 'function') {
// Prepend dependencies, and schedule to run
app.run((extension.depends || []).concat([extension]));
} else {
// If it's not a function, no implementation was given
$log.warn([
"Cannot register run extension from ",
(extension.bundle || {}).path,
"; no implementation."
].join(""));
}
}
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing
function registerComponents(components) {
return new ServiceCompositor(app, $log)
.registerCompositeServices(components);
}
// Utility; create a function which converts another function
// (which acts on single objects) to one which acts upon arrays.
function mapUpon(func) {
return function (array) {
return array.map(func);
};
}
// More like key-value pairs than methods; key is the
// name of the extension category to be handled, and the value
// is the function which handles it.
return {
constants: mapUpon(registerConstant),
routes: mapUpon(registerRoute),
directives: mapUpon(new CustomRegistrar("directive")),
controllers: mapUpon(new CustomRegistrar("controller")),
services: mapUpon(new CustomRegistrar("service")),
runs: mapUpon(registerRun),
components: registerComponents
}; };
} }
// Used to create custom registration functions which map to
// named methods on Angular modules, which follow the normal
// app.method(key, [ deps..., function ]) pattern.
function customRegistrar(angularFunction) {
return function (extension, index) {
var app = this.app,
$log = this.$log,
key = extension.key,
dependencies = extension.depends || [];
if (!key) {
$log.warn([
"Cannot register ",
angularFunction,
" ",
index,
", no key specified. ",
JSON.stringify(extension)
].join(""));
} else {
$log.info([
"Registering ",
angularFunction,
": ",
key
].join(""));
app[angularFunction](
key,
dependencies.concat([extension])
);
}
};
}
function registerConstant(extension) {
var app = this.app,
$log = this.$log,
key = extension.key,
value = extension.value;
if (typeof key === "string" && value !== undefined) {
$log.info([
"Registering constant: ",
key,
" with value ",
value
].join(""));
app.constant(key, value);
} else {
$log.warn([
"Cannot register constant ",
key,
" with value ",
value
].join(""));
}
}
// Custom registration function for extensions of category "runs"
function registerRun(extension) {
var app = this.app,
$log = this.$log;
if (typeof extension === 'function') {
// Prepend dependencies, and schedule to run
app.run((extension.depends || []).concat([extension]));
} else {
// If it's not a function, no implementation was given
$log.warn([
"Cannot register run extension from ",
(extension.bundle || {}).path,
"; no implementation."
].join(""));
}
}
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var app = this.app,
$log = this.$log,
route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing
function registerComponents(components) {
var app = this.app,
$log = this.$log;
return new ServiceCompositor(app, $log)
.registerCompositeServices(components);
}
// Utility; create a function which converts another function
// (which acts on single objects) to one which acts upon arrays.
function mapUpon(func) {
return function (array) {
return array.map(bind(func, this));
};
}
// More like key-value pairs than methods; key is the
// name of the extension category to be handled, and the value
// is the function which handles it.
/**
* Register constant values.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.constants =
mapUpon(registerConstant);
/**
* Register Angular routes.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.routes =
mapUpon(registerRoute);
/**
* Register Angular directives.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.directives =
mapUpon(customRegistrar("directive"));
/**
* Register Angular controllers.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.controllers =
mapUpon(customRegistrar("controller"));
/**
* Register Angular services.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.services =
mapUpon(customRegistrar("service"));
/**
* Register functions which will run after bootstrapping.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.runs =
mapUpon(registerRun);
/**
* Register components of composite services.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.components =
registerComponents;
return CustomRegistrars; return CustomRegistrars;
} }
); );

View File

@ -47,14 +47,41 @@ define(
// Track which extension categories have already been registered. // Track which extension categories have already been registered.
// Exceptions will be thrown if the same extension category is // Exceptions will be thrown if the same extension category is
// registered twice. // registered twice.
var registeredCategories = {}; this.registeredCategories = {};
this.customRegistrars = customRegistrars || {};
this.app = app;
this.sorter = sorter;
this.$log = $log;
}
/**
* Register a group of resolved extensions with the Angular
* module managed by this registrar.
*
* For convenient chaining (particularly from the framework
* initializer's perspective), this returns the Angular
* module with which extensions were registered.
*
* @param {Object.<string, object[]>} extensionGroup an object
* containing key-value pairs, where keys are extension
* categories and values are arrays of resolved
* extensions
* @returns {angular.Module} the application module with
* which extensions were registered
*/
ExtensionRegistrar.prototype.registerExtensions = function (extensionGroup) {
var registeredCategories = this.registeredCategories,
customRegistrars = this.customRegistrars,
app = this.app,
sorter = this.sorter,
$log = this.$log;
// Used to build unique identifiers for individual extensions, // Used to build unique identifiers for individual extensions,
// so that these can be registered separately with Angular // so that these can be registered separately with Angular
function identify(category, extension, index) { function identify(category, extension, index) {
var name = extension.key ? var name = extension.key ?
("extension-" + extension.key + "#" + index) : ("extension-" + extension.key + "#" + index) :
("extension#" + index); ("extension#" + index);
return category + "[" + name + "]"; return category + "[" + name + "]";
} }
@ -76,8 +103,8 @@ define(
function makeServiceArgument(category, extension) { function makeServiceArgument(category, extension) {
var dependencies = extension.depends || [], var dependencies = extension.depends || [],
factory = (typeof extension === 'function') ? factory = (typeof extension === 'function') ?
new PartialConstructor(extension) : new PartialConstructor(extension) :
staticFunction(extension); staticFunction(extension);
return dependencies.concat([factory]); return dependencies.concat([factory]);
} }
@ -129,9 +156,9 @@ define(
// an extension category (e.g. is suffixed by []) // an extension category (e.g. is suffixed by [])
function isExtensionDependency(dependency) { function isExtensionDependency(dependency) {
var index = dependency.indexOf( var index = dependency.indexOf(
Constants.EXTENSION_SUFFIX, Constants.EXTENSION_SUFFIX,
dependency.length - Constants.EXTENSION_SUFFIX.length dependency.length - Constants.EXTENSION_SUFFIX.length
); );
return index !== -1; return index !== -1;
} }
@ -153,8 +180,8 @@ define(
(extension.depends || []).filter( (extension.depends || []).filter(
isExtensionDependency isExtensionDependency
).forEach(function (dependency) { ).forEach(function (dependency) {
needed[dependency] = true; needed[dependency] = true;
}); });
}); });
// Remove categories which have been provided // Remove categories which have been provided
@ -174,53 +201,29 @@ define(
findEmptyExtensionDependencies( findEmptyExtensionDependencies(
extensionGroup extensionGroup
).forEach(function (name) { ).forEach(function (name) {
$log.info("Registering empty extension category " + name); $log.info("Registering empty extension category " + name);
app.factory(name, [staticFunction([])]); app.factory(name, [staticFunction([])]);
}); });
} }
function registerExtensionGroup(extensionGroup) { // Announce we're entering a new phase
// Announce we're entering a new phase $log.info("Registering extensions...");
$log.info("Registering extensions...");
// Register all declared extensions by category // Register all declared extensions by category
Object.keys(extensionGroup).forEach(function (category) { Object.keys(extensionGroup).forEach(function (category) {
registerExtensionsForCategory( registerExtensionsForCategory(
category, category,
sorter.sort(extensionGroup[category]) sorter.sort(extensionGroup[category])
); );
}); });
// Also handle categories which are needed but not declared // Also handle categories which are needed but not declared
registerEmptyDependencies(extensionGroup); registerEmptyDependencies(extensionGroup);
// Return the application to which these extensions // Return the application to which these extensions
// have been registered // have been registered
return app; return app;
} };
customRegistrars = customRegistrars || {};
return {
/**
* Register a group of resolved extensions with the Angular
* module managed by this registrar.
*
* For convenient chaining (particularly from the framework
* initializer's perspective), this returns the Angular
* module with which extensions were registered.
*
* @param {Object.<string, object[]>} extensionGroup an object
* containing key-value pairs, where keys are extension
* categories and values are arrays of resolved
* extensions
* @returns {angular.Module} the application module with
* which extensions were registered
* @memberof platform/framework.ExtensionRegistrar#
*/
registerExtensions: registerExtensionGroup
};
}
return ExtensionRegistrar; return ExtensionRegistrar;
} }

View File

@ -38,6 +38,17 @@ define(
* @constructor * @constructor
*/ */
function ExtensionSorter($log) { function ExtensionSorter($log) {
this.$log = $log;
}
/**
* Sort extensions according to priority.
*
* @param {object[]} extensions array of resolved extensions
* @returns {object[]} the same extensions, in priority order
*/
ExtensionSorter.prototype.sort = function (extensions) {
var $log = this.$log;
// Handle unknown or malformed priorities specified by extensions // Handle unknown or malformed priorities specified by extensions
function unrecognizedPriority(extension) { function unrecognizedPriority(extension) {
@ -68,7 +79,7 @@ define(
// Should be a number; otherwise, issue a warning and // Should be a number; otherwise, issue a warning and
// fall back to default priority level. // fall back to default priority level.
return (typeof priority === 'number') ? return (typeof priority === 'number') ?
priority : unrecognizedPriority(extension); priority : unrecognizedPriority(extension);
} }
// Attach a numeric priority to an extension; this is done in // Attach a numeric priority to an extension; this is done in
@ -98,22 +109,11 @@ define(
return (b.priority - a.priority) || (a.index - b.index); return (b.priority - a.priority) || (a.index - b.index);
} }
return { return (extensions || [])
/** .map(prioritize)
* Sort extensions according to priority. .sort(compare)
* .map(deprioritize);
* @param {object[]} extensions array of resolved extensions };
* @returns {object[]} the same extensions, in priority order
* @memberof platform/framework.ExtensionSorter#
*/
sort: function (extensions) {
return (extensions || [])
.map(prioritize)
.sort(compare)
.map(deprioritize);
}
};
}
return ExtensionSorter; return ExtensionSorter;
} }

View File

@ -37,8 +37,30 @@ define(
* @constructor * @constructor
*/ */
function ServiceCompositor(app, $log) { function ServiceCompositor(app, $log) {
var latest = {}, this.latest = {};
providerLists = {}; // Track latest services registered this.providerLists = {}; // Track latest services registered
this.app = app;
this.$log = $log;
}
/**
* Register composite services with Angular. This will build
* up a dependency hierarchy between providers, aggregators,
* and/or decorators, such that a dependency upon the service
* type they expose shall be satisfied by their fully-wired
* whole.
*
* Note that this method assumes that a complete set of
* components shall be provided. Multiple calls to this
* method may not behave as expected.
*
* @param {Array} components extensions of category component
*/
ServiceCompositor.prototype.registerCompositeServices = function (components) {
var latest = this.latest,
providerLists = this.providerLists,
app = this.app,
$log = this.$log;
// Log a warning; defaults to "no service provided by" // Log a warning; defaults to "no service provided by"
function warn(extension, category, message) { function warn(extension, category, message) {
@ -200,33 +222,13 @@ define(
registerLatest(); registerLatest();
} }
// Initial point of entry; just separate components by type // Initial point of entry; split into three component types.
function registerCompositeServices(components) { registerComposites(
registerComposites( components.filter(hasType("provider")),
components.filter(hasType("provider")), components.filter(hasType("aggregator")),
components.filter(hasType("aggregator")), components.filter(hasType("decorator"))
components.filter(hasType("decorator")) );
); };
}
return {
/**
* Register composite services with Angular. This will build
* up a dependency hierarchy between providers, aggregators,
* and/or decorators, such that a dependency upon the service
* type they expose shall be satisfied by their fully-wired
* whole.
*
* Note that this method assumes that a complete set of
* components shall be provided. Multiple calls to this
* method may not behave as expected.
*
* @param {Array} components extensions of category component
* @memberof platform/framework.ServiceCompositor#
*/
registerCompositeServices: registerCompositeServices
};
}
return ServiceCompositor; return ServiceCompositor;
} }

View File

@ -38,8 +38,27 @@ define(
* @constructor * @constructor
*/ */
function BundleResolver(extensionResolver, requireConfigurator, $log) { function BundleResolver(extensionResolver, requireConfigurator, $log) {
this.extensionResolver = extensionResolver;
this.requireConfigurator = requireConfigurator;
this.$log = $log;
}
/** /**
* Resolve all extensions exposed by these bundles.
*
* @param {Bundle[]} bundles the bundles to resolve
* @returns {Promise.<Object.<string, object[]>>} an promise
* for an object containing
* key-value pairs, where keys are extension
* categories and values are arrays of resolved
* extensions belonging to those categories
*/
BundleResolver.prototype.resolveBundles = function (bundles) {
var extensionResolver = this.extensionResolver,
requireConfigurator = this.requireConfigurator,
$log = this.$log;
/*
* Merge resolved bundles (where each is expressed as an * Merge resolved bundles (where each is expressed as an
* object containing key-value pairs, where keys are extension * object containing key-value pairs, where keys are extension
* categories and values are arrays of resolved extensions) * categories and values are arrays of resolved extensions)
@ -99,28 +118,13 @@ define(
.then(giveResult); .then(giveResult);
} }
return { // First, make sure Require is suitably configured
/** requireConfigurator.configure(bundles);
* Resolve all extensions exposed by these bundles.
*
* @param {Bundle[]} bundles the bundles to resolve
* @returns {Promise.<Object.<string, object[]>>} an promise
* for an object containing
* key-value pairs, where keys are extension
* categories and values are arrays of resolved
* extensions belonging to those categories
* @memberof platform/framework.BundleResolver#
*/
resolveBundles: function (bundles) {
// First, make sure Require is suitably configured
requireConfigurator.configure(bundles);
// Then, resolve all extension implementations. // Then, resolve all extension implementations.
return Promise.all(bundles.map(resolveBundle)) return Promise.all(bundles.map(resolveBundle))
.then(mergeResolvedBundles); .then(mergeResolvedBundles);
} };
};
}
return BundleResolver; return BundleResolver;
} }

View File

@ -39,6 +39,27 @@ define(
* @constructor * @constructor
*/ */
function ExtensionResolver(loader, $log) { function ExtensionResolver(loader, $log) {
this.loader = loader;
this.$log = $log;
}
/**
* Resolve the provided extension; this will give a promise
* for the extension's implementation, if one has been
* specified, or for the plain definition of the extension
* otherwise. The plain definition will also be given
* if the implementation fails to load for some reason.
*
* All key-value pairs from the extension definition
* will additionally be attached to any loaded implementation.
*
* @param {Extension} extension the extension to resolve
* @returns {Promise} a promise for the resolved extension
*/
ExtensionResolver.prototype.resolve = function (extension) {
var loader = this.loader,
$log = this.$log;
function loadImplementation(extension) { function loadImplementation(extension) {
var implPath = extension.getImplementationPath(), var implPath = extension.getImplementationPath(),
implPromise = loader.load(implPath), implPromise = loader.load(implPath),
@ -57,8 +78,8 @@ define(
// loaded implementation. // loaded implementation.
function attachDefinition(impl) { function attachDefinition(impl) {
var result = (typeof impl === 'function') ? var result = (typeof impl === 'function') ?
constructorFor(impl) : constructorFor(impl) :
Object.create(impl); Object.create(impl);
// Copy over static properties // Copy over static properties
Object.keys(impl).forEach(function (k) { Object.keys(impl).forEach(function (k) {
@ -84,11 +105,11 @@ define(
function handleError(err) { function handleError(err) {
// Build up a log message from parts // Build up a log message from parts
var message = [ var message = [
"Could not load implementation for extension ", "Could not load implementation for extension ",
extension.getLogName(), extension.getLogName(),
" due to ", " due to ",
err.message err.message
].join(""); ].join("");
// Log that the extension was not loaded // Log that the extension was not loaded
$log.warn(message); $log.warn(message);
@ -107,33 +128,16 @@ define(
return implPromise.then(attachDefinition, handleError); return implPromise.then(attachDefinition, handleError);
} }
return { // Log that loading has begun
/** $log.info([
* Resolve the provided extension; this will give a promise "Resolving extension ",
* for the extension's implementation, if one has been extension.getLogName()
* specified, or for the plain definition of the extension ].join(""));
* otherwise. The plain definition will also be given
* if the implementation fails to load for some reason.
*
* All key-value pairs from the extension definition
* will additionally be attached to any loaded implementation.
*
* @param {Extension} extension
* @memberof platform/framework.ExtensionResolver#
*/
resolve: function (extension) {
// Log that loading has begun
$log.info([
"Resolving extension ",
extension.getLogName()
].join(""));
return extension.hasImplementation() ? return extension.hasImplementation() ?
loadImplementation(extension) : loadImplementation(extension) :
Promise.resolve(extension.getDefinition()); Promise.resolve(extension.getDefinition());
} };
};
}
return ExtensionResolver; return ExtensionResolver;
} }

View File

@ -39,31 +39,27 @@ define(
* @param {*} $log Angular's logging service * @param {*} $log Angular's logging service
*/ */
function ImplementationLoader(require) { function ImplementationLoader(require) {
function loadModule(path) { this.require = require;
return new Promise(function (fulfill, reject) {
require([path], fulfill, reject);
});
}
return {
/**
* Load an extension's implementation; or, equivalently,
* load an AMD module. This is fundamentally similar
* to a call to RequireJS, except that the result is
* wrapped in a promise. The promise will be fulfilled
* with the loaded module, or rejected with the error
* reported by Require.
*
* @method
* @memberof ImplementationLoader#
* @param {string} path the path to the module to load
* @returns {Promise} a promise for the specified module.
* @memberof platform/framework.ImplementationLoader#
*/
load: loadModule
};
} }
/**
* Load an extension's implementation; or, equivalently,
* load an AMD module. This is fundamentally similar
* to a call to RequireJS, except that the result is
* wrapped in a promise. The promise will be fulfilled
* with the loaded module, or rejected with the error
* reported by Require.
*
* @param {string} path the path to the module to load
* @returns {Promise} a promise for the specified module.
*/
ImplementationLoader.prototype.load = function loadModule(path) {
var require = this.require;
return new Promise(function (fulfill, reject) {
require([path], fulfill, reject);
});
};
return ImplementationLoader; return ImplementationLoader;
} }
); );

View File

@ -35,79 +35,79 @@ define(
* @param requirejs an instance of RequireJS * @param requirejs an instance of RequireJS
*/ */
function RequireConfigurator(requirejs) { function RequireConfigurator(requirejs) {
// Utility function to clone part of a bundle definition this.requirejs = requirejs;
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Look up module configuration from the bundle definition.
// This will adjust paths to libraries as-needed.
function getConfiguration(bundle) {
var configuration = bundle.getConfiguration();
// Adjust paths to point to libraries
if (configuration.paths) {
// Don't modify the actual bundle definition...
configuration = clone(configuration);
// ...replace values in a clone instead.
Object.keys(configuration.paths).forEach(function (path) {
configuration.paths[path] =
bundle.getLibraryPath(configuration.paths[path]);
});
}
return configuration;
}
// Build up paths and shim values from multiple bundles;
// this is sensitive to the value from baseConfiguration
// passed via reduce in buildConfiguration below, insofar
// as it assumes paths and shim will have initial empty values.
function mergeConfigurations(base, next) {
["paths", "shim"].forEach(function (k) {
Object.keys(next[k] || {}).forEach(function (p) {
base[k][p] = next[k][p];
});
});
return base;
}
// Build a configuration object, to pass to requirejs.config,
// based on the defined configurations for all bundles.
// The paths and shim properties from all bundles will be
// merged to allow one requirejs.config call.
function buildConfiguration(bundles) {
// Provide an initial requirejs configuration...
var baseConfiguration = {
baseUrl: "",
paths: {},
shim: {}
},
// ...and pull out all bundle-specific parts
bundleConfigurations = bundles.map(getConfiguration);
// Reduce this into one configuration object.
return bundleConfigurations.reduce(
mergeConfigurations,
baseConfiguration
);
}
return {
/**
* Configure RequireJS to utilize any path/shim definitions
* provided by these bundles.
*
* @param {Bundle[]} the bundles to include in this
* configuration
* @memberof platform/framework.RequireConfigurator#
*/
configure: function (bundles) {
return requirejs.config(buildConfiguration(bundles));
}
};
} }
// Utility function to clone part of a bundle definition
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Look up module configuration from the bundle definition.
// This will adjust paths to libraries as-needed.
function getConfiguration(bundle) {
var configuration = bundle.getConfiguration();
// Adjust paths to point to libraries
if (configuration.paths) {
// Don't modify the actual bundle definition...
configuration = clone(configuration);
// ...replace values in a clone instead.
Object.keys(configuration.paths).forEach(function (path) {
configuration.paths[path] =
bundle.getLibraryPath(configuration.paths[path]);
});
}
return configuration;
}
// Build up paths and shim values from multiple bundles;
// this is sensitive to the value from baseConfiguration
// passed via reduce in buildConfiguration below, insofar
// as it assumes paths and shim will have initial empty values.
function mergeConfigurations(base, next) {
["paths", "shim"].forEach(function (k) {
Object.keys(next[k] || {}).forEach(function (p) {
base[k][p] = next[k][p];
});
});
return base;
}
// Build a configuration object, to pass to requirejs.config,
// based on the defined configurations for all bundles.
// The paths and shim properties from all bundles will be
// merged to allow one requirejs.config call.
function buildConfiguration(bundles) {
// Provide an initial requirejs configuration...
var baseConfiguration = {
baseUrl: "",
paths: {},
shim: {}
},
// ...and pull out all bundle-specific parts
bundleConfigurations = bundles.map(getConfiguration);
// Reduce this into one configuration object.
return bundleConfigurations.reduce(
mergeConfigurations,
baseConfiguration
);
}
/**
* Configure RequireJS to utilize any path/shim definitions
* provided by these bundles.
*
* @param {Bundle[]} the bundles to include in this
* configuration
* @memberof platform/framework.RequireConfigurator#
*/
RequireConfigurator.prototype.configure = function (bundles) {
return this.requirejs.config(buildConfiguration(bundles));
};
return RequireConfigurator; return RequireConfigurator;
} }