action-u
Version:
Redux Action Generator (promoting action creators and types)
427 lines (382 loc) • 17 kB
JavaScript
exports.__esModule = true;
exports.default = generateActions;
var _lodash = require('lodash.isfunction');
var _lodash2 = _interopRequireDefault(_lodash);
var _lodash3 = require('lodash.isplainobject');
var _lodash4 = _interopRequireDefault(_lodash3);
var _verify = require('../util/verify');
var _verify2 = _interopRequireDefault(_verify);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
/**
* A higher-order function that mirrors the supplied
* {{book.api.actionGenesis}} structure, returning an
* {{book.api.ActionStruct}}, injecting generated action creators that
* are decorated with their coresponding action types.
*
* {{book.guide.formalTypes}} diagrams this process. Examples can be
* found throughout the {{book.guide.devGuide}}, starting with
* {{book.guide.basics}}.
*
* @param {ActionGenesis} actionGenesis - an "organizational" JSON
* structure that defines one or more action creators, with implicitly
* defined action types (gleaned from the structure itself).
*
* @returns {ActionStruct} an action structure (a mirror of
* {{book.api.actionGenesis}}), with generated action creators that
* are decorated with their cooresponding action types.
*/
function generateActions(actionGenesis) {
// validate actionGenesis
var check = _verify2.default.prefix('ActionU.generateActions() parameter violation: ');
check(actionGenesis, 'actionGenesis argument is required');
check((0, _lodash4.default)(actionGenesis), 'actionGenesis argument is NOT an object literal');
check(!actionGenesis.actionMeta, "actionGenesis argument CANNOT have an actionMeta property in the root (i.e. the root cannot be an ActionNode, because it is unnamed)");
// morph the supplied actionGenesis into an ActionStruct
return morph2Runtime(actionGenesis, '');
}
/**
* The `generateActions.root()` function is identical to
* {{book.api.generateActions}}, except it returns the single root
* node of the {{book.api.ActionStruct}}, rather than the entire
* structure. *This is useful for projects that organize their
* actions in seperate JavaScript modules (see
* {{book.guide.promotion}})*.
*
* @param {ActionGenesis} actionGenesis - an "organizational" JSON
* structure that defines one or more action creators, with implicitly
* defined action types (gleaned from the structure itself). For
* `generateActions.root()`, this is **expected** to contain a single
* root node (which will be returned).
*
* @returns {ActionStruct} **the root** action structure (a mirror of
* {{book.api.actionGenesis}}), with generated action creators that
* are decorated with their cooresponding action types.
*/
generateActions.root = function (actionGenesis) {
// pass-through to generateActions()
var actionStruct = generateActions(actionGenesis);
// validae only one root node
var rootNodeNames = Object.keys(actionGenesis); // ... use actionGenesis so as NOT to get the injected 'toString' of actionStruct
var check = _verify2.default.prefix('ActionU.generateActions.root() parameter violation: ');
check(rootNodeNames.length === 1, 'actionGenesis argument may ONLY contain a single root node (what will be returned) ... ' + rootNodeNames);
// expose the ActionStruct root
return actionStruct[rootNodeNames[0]];
};
//***
//*** Internals ...
//***
var knownMetaProps = ['traits', 'ratify', 'thunk'];
/**
* Our default ActionMeta.ratify() function ... simply no-op.
* @private
*/
function ratifyDefault() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return args;
}
/**
* Morph the supplied genesisNode into a runtime node ... part of the
* ActionStruct.
*
* @param {JSON-node} genesisNode - a node within the ActionGenisis
* structure to be "morphed".
*
* @param {string} actionType - the accumulative action type
* representing this genesisNode (within the overal ActionGenesis).
*
* @returns {JSON-node} a "morphed" run-time node representation of
* the supplied genesisNode.
*
* @private
*/
function morph2Runtime(genesisNode, actionType) {
// validate genesisNode
var check = _verify2.default.prefix('ActionU.generateActions() actionGenesis node ' + actionType);
check((0, _lodash4.default)(genesisNode), ' must be an object literal');
check(Object.keys(genesisNode).length > 0, ' must contain at least ONE sub-node (either an app-specific or an actionMeta node)');
// define our actionMeta (if any)
var actionMeta = genesisNode.actionMeta;
// morph the genesisNode into a runtimeNode (of the ActionStruct)
var runtimeNode = null;
if (actionMeta) {
// *** node is an action creator (i.e. an ActionNode)
// insure actionMeta is an object literal
check((0, _lodash4.default)(actionMeta), '.actionMeta is NOT an object literal');
// insure all actionMeta properties are known
var metaProps = Object.keys(actionMeta);
var unknownMetaProps = metaProps.filter(function (prop) {
return knownMetaProps.indexOf(prop) < 0;
});
check(unknownMetaProps.length === 0, '.actionMeta contains unrecognized properties: ' + unknownMetaProps);
// thunk definition
if (actionMeta.thunk) {
// insure actionMeta.thunk is a function
check((0, _lodash2.default)(actionMeta.thunk), '.actionMeta.thunk is NOT a function ... ' + actionMeta.thunk);
// insure no other actionMeta properties are provided
check(metaProps.length === 1, '.actionMeta.thunk is NOT allowed with any other actionMeta property (found following properties: ' + metaProps + ')');
// thunks are simply injected directly in our ActionStruct (as is)
runtimeNode = actionMeta.thunk;
}
// action creator generation (i.e. NON thunk)
else {
// insure actionMeta.traits (if supplied) is a string[]
var traits = actionMeta.traits || [];
check(Array.isArray(traits), '.actionMeta.traits is NOT a string[]'); // consider also lodash.isString() on each elm
// insure actionMeta.ratify (if supplied) is a function
var ratify = actionMeta.ratify || ratifyDefault;
check((0, _lodash2.default)(ratify), '.actionMeta.ratify is NOT a function ... ' + ratify);
// ***
// *** THIS IS IT ... here is a generated action creator (i.e. an ActionNode)
// ***
runtimeNode = function runtimeNode() {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
// apply app-specific action creator parameter validation/defaults
args = ratify.apply(undefined, _toConsumableArray(args));
// apply standard validation (insuring correct number of arguments passed in)
if (traits.length !== args.length) {
// ex: ERROR: action-u action creator: userMsg.display(msg) expecting 1 parameters, but received 0.
throw new TypeError('ERROR: action-u action creator: ' + actionType + '(' + traits.toString() + ') expecting ' + traits.length + ' parameters, but received ' + args.length + '.');
}
// build/return our action object
var action = { type: actionType }; // baseline action with it's type
for (var i = 0; i < args.length; i++) {
// inject the arguments into our action
action[traits[i]] = args[i];
}
return action;
};
}
// overload toString() to promote our action type
runtimeNode.toString = function () {
return actionType;
};
} else {
// *** node is an app-specific node (who's sub-structure will contain ActionNodes)
// this is an app-specific node (who's sub-structure will contain ActionNodes)
runtimeNode = {};
// overload toString() to ERROR (it is NOT an action type)
// ex: ERROR: action-u ActionStruct: 'userMsg' is NOT an action type, RATHER an app-specific node.
runtimeNode.toString = function () {
throw new TypeError('ERROR: action-u ActionStruct: \'' + actionType + '\' is NOT an action type (rather an app-specific node).');
};
}
// recurse further into the sub-structure
for (var subNodeName in genesisNode) {
if (subNodeName !== 'actionMeta') {
// do NOT process actionMeta nodes - they are NOT app-specific
// ... internal use only (processed by parent node)
var subNode = genesisNode[subNodeName];
var delim = actionType ? '.' : '';
var subType = '' + actionType + delim + subNodeName;
// morph nested sub-structure
runtimeNode[subNodeName] = (0, _lodash4.default)(subNode) ? morph2Runtime(subNode, subType) : subNode; // support app-specific data in struct (just copy as is)
// ... currently undocumented (because I'm not sure it is needed)
}
}
// that's all folks :-)
return runtimeNode;
}
//***
//*** Specifications (JSDoc tags only)
//***
/**
* @typedef {JSON} ActionGenesis
*
* ActionGenesis is a JSON meta structure (used by
* {{book.api.generateActions}}) that provides the master definition
* for the generated {{book.api.ActionStruct}}, promoting one or more
* action creators and types.
*
* {{book.guide.formalTypes}} diagrams the action-u formal types.
*
* - The structure is app-specific and can employ depth to highlight
* inner-relationships between various action creators.
*
* - Ultimately the structure defines one or more action creator
* function nodes. Each of these nodes promote:
*
* * the action creator function itself, and
*
* * the action type, which is implicitly gleaned from the containing JSON
* structure node accumulation (ex: `'widget.fetch'`)
*
* - Nodes containing an {{book.api.actionMeta}} property define an
* {{book.api.ActionNode}} (i.e. an action creator).
*
* * The resultant corresponding node will be an action creator
* function. The characteristics of this function is further
* defined by {{book.api.actionMeta}} sub-properties (see
* {{book.api.ActionMeta}}).
*
* * The action type is implied from the containing JSON structure
* node accumulation (ex: `'widget.fetch.complete'`) and is
* promoted through a string coercion of the action creator
* function itself (i.e. the function's toString() has been
* overloaded).
*
* - All other nodes are merely intermediate nodes that organize
* (i.e. add meaning) to the overall shape of the promoted actions.
* In the example below, `widget` is an intermediate node (i.e. it
* is not an action creator).
*
* - Note that even {{book.api.ActionNodes}} may in turn contain
* sub-structure (i.e. subordinate actions). In the example below,
* `widget.fetch(selCrit)` is an action creator, an yet contains
* subordinate actions: `widget.fetch.complete(widget)`.
*
* @example <caption>showing a standard set of fetch/complete/fail actions</caption>
* {
* widget: {
* fetch: {
* actionMeta: {
* traits: ['selCrit']
* },
* complete: {
* actionMeta: {
* traits: ['widget']
* }
* },
* fail: {
* actionMeta: {
* traits: ['err']
* }
* }
* }
* }
* }
*/
/**
* @typedef {JSON} ActionMeta
*
* An ActionMeta is a sub-node (named `actionMeta`) in the
* {{book.api.ActionGenesis}} that identifies it's parent as being an
* {{book.api.ActionNode}} (i.e. an action creator).
*
* {{book.guide.formalTypes}} diagrams the action-u formal types.
*
* Supported properties of ActionMeta are:
*
* @property {string[]} traits - An array of names that serve BOTH as the:
* <ul>
* <li>expected parameter names of the action creator</li>
* <li>and the Action property names (returned from the action creator)</li>
* When NO `traits` property is supplied, the Action merely has NO properties
* (other than it's `type` [of course]).
* </ul>
* Please refer to the {{book.guide.basics}} discussion for complete examples.
*
* @property {ratifyFn} ratify - An optional hook to validate and/or
* default action creator parameters.<br/>
* When NO `ratify` function is supplied, only simple validation is
* performed *(ex: the number of arguments supplied)*. Please refer
* to {{book.guide.validation}} and {{book.guide.default}} for
* complete examples.
*
* @property {function} thunk - An action creator function that
* promotes a thunk. When `thunk` is used, no other ActionMeta
* properties are allowed. Please refer to {{book.guide.thunks}} for
* a complete description.
*/
/**
* @function ratifyFn
*
* @description
* An optional hook of {{book.api.ActionMeta}} to validate and/or default
* action creator parameters.
*
* - validation is accomplished by app-specific means (typically
* thrown exceptions)
*
* - default parameters are accomplished by applying default semantics
* and returning the arguments
*
* Please refer to {{book.guide.validation}} and
* {{book.guide.default}} for complete examples.
*
* @param {...*} args - the parameters to this function should match
* that of the action creator it is defining
*
* @returns {args} an array of the arguments passed in (potentially
* defaulted). **NOTE**: You should never attempt to return the
* built-in `arguments` array-like object for two reasons: **1.**
* applied defaults are NOT reflected in `arguments`, and **2.**
* `arguments` are not bound to arrow functions.
*/
/**
* @typedef {JSON} ActionStruct
*
* ActionStruct is a JSON stucture which is a key aspect of action-u.
* It:
* - implicitly defines your action types,
* - instinctively groups related actions,
* - and seamlessly promotes both action creators and types throughout
* your application.
*
* ActionStruct is a generated JSON run-time structure (output from
* {{book.api.generateActions}}) that promotes a series of action
* creators and types in an app-specific structure (mirroring the
* shape of the {{book.api.ActionGenesis}}).
*
* {{book.guide.formalTypes}} diagrams the action-u formal types.
*
* - The structure is app-specific and can employ depth to highlight
* inner-relationships between various action creators.
*
* - The structure defines one or more {{book.api.ActionNodes}}
* (i.e. action creator functions). Each {{book.api.ActionNode}}
* encapsolates BOTH the action creator and it's type.
*
* * The action creator function (the node itself) accepts the
* desired parameters and returns a newly created action.
*
* * The action type is implied from the containing JSON structure
* node accumulation (ex: `'widget.fetch.complete'`) and is
* promoted through a string coercion of the action creator
* function itself (i.e. the function's toString() has been
* overloaded).
*
* - All other nodes are merely intermediate nodes that organize
* (i.e. add meaning) to the overall shape of the promoted actions.
* In the example below, `widget` is an intermediate node (i.e. it
* is not an action creator).
*
* - Note that even {{book.api.ActionNodes}} may in turn contain sub-structure
* (i.e. subordinate actions). In the example below,
* `widget.fetch(selCrit)` is an action creator, an yet contains
* subordinate actions: `widget.fetch.complete(widget)`.
*
* @example <caption>showing a standard set of fetch/complete/fail actions</caption>
* {
* widget: {
* fetch(selCrit): { // action creator (impl omitted) - type promoted via string coercion of funct
* complete(widget): {} // action creator (impl omitted) - type promoted via string coercion of funct
* fail(err): {} // action creator (impl omitted) - type promoted via string coercion of funct
* }
* }
* }
*/
/**
* @function ActionNode
*
* @description
* ActionNode is a generated action creator function that lives as a
* JSON node in the {{book.api.ActionStruct}}.
*
* The ActionNode promotes it's action type through a string coercion
* of the action creator function itself (i.e. the function's
* toString() has been overloaded).
*
* {{book.guide.formalTypes}} diagrams the action-u formal types.
*
* @param {...*} args - the parameters are app-specific to this action
* type.
*
* @returns {Action} a standard redux Action, specific to this action
* type.
*/
;