Gearbox
Gearbox is a Node.js framework for assembling pretty-much anything from an extensible set of components. Within Gearbox, these components are known as Gears. There's a growing library of pre-canned Gears available on NPM and a Yeoman Generator to help roll your own.
Contents
Gearbox Module
Installation
$ npm install gearbox
Gear Prelude
Gearbox delivers functionality via configurable components referred to as 'Gears'.
As explained later, Gears are little-more than Node.js modules that provide some bounded functionality.
Gears can offer something as small as a textBox for use in a web form, through to something as big as a web server.
Though a few internal Gears come bundled with the main Gearbox module (e.g. a logger
), a one-NPM-package-per-Gear approach is employed for everything else.
- Gears can inherit properties from other Gears
- Gears can have other Gear instances injected into them
- Gears can be arranged into tree structures
Basic usage
var Gearbox = ; var box = ; // Create a new Gearbox instance // Get a 'car' gearbox;
Configuration
When creating a new Gearbox instance, it's possible to provide an optional configuration object:
var box = config;
gearConfig
- Required: No
- Type: Object
Description
This is one of several ways to configure Gears - it's especially useful for configuring Gearbox-wide singleton-Gears such as a logger or a database connection.
- The keys of the configuration-object should be a Gear name, and the value should be an object that will be passed as config when the Gear is instantiated.
- Providing configuration for a Gear does not imply it will ever get instantiated... only when a new Gear instance is required will this config be consulted.
Example
var Gearbox = ;var box = gearConfig: logger: level: 'debug' database: dataSource: connector: 'postgresql' host: 'localhost' port: 5432 database: 'gearbox' username: 'gearbox' password: 'gearbox' ;
- Here, when a
logger
Gear is first required, it will be configured with alevel
ofdebug
, and if adatabase
Gear is required, it will be instantiated with adataSource
object. - Gears which are designed to behave like singletons are usually configured in this way.
modulePrefixes
- Required: No
- Default:
['gear']
- Type: Array of Strings
Description
If all fails, Gearbox will use Node's internal machinery to require
a Gear module (as described here).
By default the requested Gear name will be turned into a module name with a prepended gear-
, but if you're developing your own Gears you may prefer to use your own prefixes.
This can be achieved by providing alternative modulePrefixes
, Gearbox will iterate over the provided prefixes in order, looking for a resolvable module.
Example
{
modulePrefixes: ['funky-startup', 'gear']
}
maxDepth
- Required: No
- Default:
100
- Type: Integer
Description
This places a limit on the recursive depth things will be allowed to descend when constructing dependencies. Pretty internal - probably doesn't need changing.
maxConstructors
- Required: No
- Default:
1500
- Type: Integer
Description
The maximum number of cached Gear-constructors that will be held in memory at any one time.
maxSingletons
- Required: No
- Default:
1500
- Type: Integer
Description
The maximum number of cached singleton instances that will be held in memory at any one time.
maxModules
- Required: No
- Default:
1500
- Type: Integer
Description
The maximum number of 'loaded' Gear modules that will be held in memory at any one time.
path
- Required: No
- Default:
[]
- Type: Array of Strings
Description
registeredGearDirs
- Required: No
- Default:
{}
- Type: Object
Description
Gearbox API
registerGearPath(gearName, absDir, relDir)
Explicitly signposts the directory where the named Gear can be found. Gears that are registered in this way will resolve to the specified directory ahead of employing less specific discovery techniques.
Parameter | Required? | Description |
---|---|---|
gearName |
Yes | The name of the Gear to register a directory for. |
absDir |
Yes | An absolute directory path where the Gear module can be found. Can be used in conjunction with relDir . |
relDir |
No | An optional relative path to be taken from absDir . |
Example
var Gearbox = ;var box = ;box;
appendToPath(relDir, absPath)
If a Gear can't be found (because it's not been registered via registerGearPath
) then Gearbox will use an internal path looking for Gears.
The appendToPath
method simply adds another directory to this internal path.
- The directory added to the path can either signpost a Gear module directory (consider using the more explicit
registerGearPath
) or perhaps more usefully: a directory which contains many Gear module directories. In this instance, all the Gears in the containing directory are 'discoverable'.
Parameter | Required? | Description |
---|---|---|
dir |
Yes | Either a relative or absolute directory path. |
absDir |
No | If provided it's assumed the contents of dir is a relative directory path, and will be resolved from absDir . |
Example
var Gearbox = ;var box = ;box;
discover(callback)
Once the internal path has all the required directories appended to it (via appendToPath
) then calling discover
will trawl the various directories and add any Gears it finds.
Parameter | Required? | Description |
---|---|---|
callback |
Yes | Simple function(err) callback. |
Example
var Gearbox = ;var box = ;box;box;
get(gearName, instanceConfig, callback)
Gets a fully instantiated Gear instance, with all dependencies injected and any Gear-inheritance taken care of.
- If the Gear behaves like a singletons (e.g. a logger) the single Gearbox-wide instance will be returned - otherwise a fresh instance will be created.
Parameter | Required? | Description |
---|---|---|
gearName |
Yes | Name of the Gear to get |
instanceConfig |
No | If the Gear is not a singleton, it's possible to specify config for the fresh instance here. This is a simple config object. |
callback |
Yes | Of the form function (err, gear) where if successful gear is the instantiated Gear instance. |
Example
var Gearbox = ;var box = ; // Create a new Gearbox instance // Get a 'car' gearbox;
Gears
Overview
Loading Gears
Gears are nothing more than Node.js modules which export an object with certain properties.
If you encounter a GearError: [unresolvedGear] Can't find module for name 'someMadeUpGearName'
then Gearbox has failed to require
a suitable Gear module.
Troubleshooting
For Gearbox to successfully load a Gear, at least one of the following must be true:
- The Gear is one of the few internal Gears.
- The directory containing the Gear's module has been registered with
gearbox.registerGearPath()
. - The directory containing the Gear's module is either directly in Gearbox's internal path, or a direct sub-directory from a directory specified in Gearbox's internal path.
- Either use
gearbox.appendToPath()
to add new directories to Gearbox's internal path - Or specify absolute directories as part of the Gearbox config object.#
- Regardless of which path building method is used, always be sure to finish by calling
gearbox.discover()
.
- Either use
- If Gearbox still has no joy in finding a module, then it will rely on Node's own module loading machinery. The specified Gear name will be converted into a module name in the following way:
- First the Gear name is dasherized.
- Then at least one prefix is prepended - Gearbox will always try the
gear-
prefix.- It's possible to provide alternative prefixes via the
modulePrefixes
property in Gearbox's configuration object.
- It's possible to provide alternative prefixes via the
- So looking for a Gear named
webServer
will ultimately result in Gearbox tryingrequire('gear-web-server')
.
Export object
There's not much to a Gear... just a simple Node.js module that exports a single object. For example:
moduleexports = name: 'cow' // Name of the gear (camelcase) create: 'each' // Gears can behave as singletons or instances extending: 'farmAnimal' // Gear-inheritance is possible dependencies: 'logger' // Gear Dependency-Injection is possible allowedParents: 'barn' // Gears can be structured into trees // Each Gear can have config supplied to it... // values of which can be defaulted: defaults: milkable: true friendly: true // Config values can be validated via JSON Schema: schema: // Gears can have the equivalent of a constructor: { thislogger; } // Gears can have their own methods: { prototype { if loudness > 10} thislogger; else thislogger; }; }};
This is the full list of properties you can export in a Gear module:
name
- Required: Yes
- Type: String
Description
The unique name of the Gear within a Gearbox instance. There are a few naming conventions to adhere to:
- Letters and numbers only
- Start with a lowercase letter
- Use camelcase for multi-word gear names
- Try to keep a Gear's name short and punchy
Example
name: 'farmAnimal'
create
- Required: No
- Default:
each
- Type: String
Description
When Gearbox creates a new Gear, there are a couple of options:
Option | Description |
---|---|
one |
The Gear should be considered a singleton... only one Gear instance will ever be created. Any subsequent requests for a Gear of this type will return that same instance. For example the logger Gear has a create value of one . |
each |
A new Gear instance will be created on each request. If a set of Gears were being arranged to form a web form, then textBox would be an example of a Gear requiring a create value of each . This is the default behaviour. |
Example
name: 'farmAnimal' create: 'each'
extending
- Required: No
- Type: String
Description
Gearbox supports Gear inheritance. For example a car
Gear can inherit properties from a more general vehicle
Gear.
To achieve all this, the value of extending
should simply be the name
of another gear, perhaps better termed as a 'Supergear' (e.g. a car
Gear could have an extending
value of vehicle
).
In this continuing example, vehicle
is just a normal Gear from which several properties will be harvested when creating car
Gears:
Property | How it's inherited |
---|---|
defaults |
The car Gear's defaults will be used as usual, but any other defaults defined in vehicle will be applied as well. In the case of both Gears defining the same default value, the car Gear value will override that of vehicle . |
allowedParents |
In the case of both Gears providing allowedParents arrays, they are unioned together. If there's just one Gear providing an array, that's what is used. |
allowedChildren |
Just like allowedParents , but for children. |
methods |
Exactly the same 'override' behaviour as used in defaults , but applied to methods instead. |
implicitGears |
If vehicle provides implicitGears , then that value is used, unless car provides it - in which case there's no merging of the two arrays - the car array just wins. |
- It's quite feasible to have a
superCar
Gear, extending acar
Gear, which in-turn is extending avehicle
Gear. These rules are applied at each step along the chain.
Example
name: 'car' extending: 'vehicle'
dependencies
- Required: No
- Type: Array of strings
Description
Gearbox supports a Gear-based Dependency Injection mechanism - provide an array of Gear names via dependencies
, and fully-instantiated Gear instances will be available as simple properties when the Gear is created.
- Dependent Gears should be singletons (e.g. all the Gears referred to in the
dependencies
array should have their respectivecreate
value set toone
). - Dependency loops (
gearA
has a dependency ongearB
, yetgearB
has a dependency ongearA
) will cause an error. - The usual config machinery will be employed when creating dependencies.
Example
name: 'databaseTable' dependencies: 'logger' 'relationalDatabase'
allowedParents
- Required: No
- Type: Array of strings
Description
Blueprints provide a way to arrange Gears into a tree structure.
To help apply some integrity to these trees, placing a Gear 'under' a Gear whose type isn't named in the allowedParents
array will cause an error
(though allowedChildren
can potentially save the day).
- Include the special name
$root
in theallowedParents
array if the Gear is permissible on the root of a tree structure. - Withstanding the potential use of
allowedChildren
, if noallowedParents
is defined then trying to use the Gear as part of a tree will cause an error.
Example
name: 'databaseColumn' allowedParents: 'databaseTable'
allowedChildren
defaults
schema
initFunction
secondPassFunction
Tests
$ npm test
$ npm test --coverage