var _ = require('../util').lodash,
PropertyBase = require('./property-base').PropertyBase,
/**
* Primitive mutation types.
*
* @private
* @constant
* @type {Object}
*/
PRIMITIVE_MUTATIONS = {
SET: 'set',
UNSET: 'unset'
},
/**
* Detects if the mutation is a primitive mutation type. A primitive mutation is the simplified mutation structure.
*
* @private
* @param {MutationTracker.mutation} mutation -
* @returns {Boolean}
*/
isPrimitiveMutation = function (mutation) {
return mutation && mutation.length <= 2;
},
/**
* Applies a single mutation on a target.
*
* @private
* @param {*} target -
* @param {MutationTracker.mutation} mutation -
*/
applyMutation = function applyMutation (target, mutation) {
// only `set` and `unset` instructions are supported
// for non primitive mutations, the instruction would have to be extracted from mutation
/* istanbul ignore if */
if (!isPrimitiveMutation(mutation)) {
return;
}
// extract instruction from the mutation
var operation = mutation.length > 1 ? PRIMITIVE_MUTATIONS.SET : PRIMITIVE_MUTATIONS.UNSET;
// now hand over applying mutation to the target
target.applyMutation(operation, ...mutation);
},
MutationTracker;
/**
* A JSON representation of a mutation on an object. Here objects mean instances of postman-collection classes.
* This captures the instruction and the parameters of the instruction so that it can be replayed on a different object.
* Mutations can be any change on an object. For example setting a key or unsetting a key.
*
* For example, the mutation to set `name` on an object to 'Bruce Wayne' would look like ['name', 'Bruce Wayne']. Where
* the first item is the key path and second item is the value. To add a property `punchLine` to the object it would be
* the same as updating the property i.e. ['punchLine', 'I\'m Batman']. To remove a property `age` the mutation would
* look like ['age'].
*
* This format of representing changes is derived from
* {@link http://json-delta.readthedocs.io/en/latest/philosophy.html}.
*
* The `set` and `unset` are primitive instructions and can be derived from the mutation without explicitly stating the
* instruction. For more complex mutation the instruction would have to be explicitly stated.
*
* @typedef {Array} MutationTracker.mutation
*/
/**
* A JSON representation of the MutationTracker.
*
* @typedef MutationTracker.definition
*
* @property {Array} stream contains the stream mutations tracked
* @property {Object} compacted contains a compacted version of the mutations
* @property {Boolean} [autoCompact=false] when set to true, all new mutations would be compacted immediately
*/
_.inherit((
/**
* A MutationTracker allows to record mutations on any of object and store them. This stored mutations can be
* transported for reporting or to replay on similar objects.
*
* @constructor
* @extends {PropertyBase}
*
* @param {MutationTracker.definition} definition serialized mutation tracker
*/
MutationTracker = function MutationTracker (definition) {
// this constructor is intended to inherit and as such the super constructor is required to be executed
MutationTracker.super_.call(this, definition);
definition = definition || {};
// initialize options
this.autoCompact = Boolean(definition.autoCompact);
// restore mutations
this.stream = Array.isArray(definition.stream) ? definition.stream : [];
this.compacted = _.isPlainObject(definition.compacted) ? definition.compacted : {};
}), PropertyBase);
_.assign(MutationTracker.prototype, /** @lends MutationTracker.prototype */ {
/**
* Records a new mutation.
*
* @private
* @param {MutationTracker.mutation} mutation -
*/
addMutation (mutation) {
// bail out for empty or unsupported mutations
if (!(mutation && isPrimitiveMutation(mutation))) {
return;
}
// if autoCompact is set, we need to compact while adding
if (this.autoCompact) {
this.addAndCompact(mutation);
return;
}
// otherwise just push to the stream of mutations
this.stream.push(mutation);
},
/**
* Records a mutation compacting existing mutations for the same key path.
*
* @private
* @param {MutationTracker.mutation} mutation -
*/
addAndCompact (mutation) {
// for `set` and `unset` mutations the key to compact with is the `keyPath`
var key = mutation[0];
// convert `keyPath` to a string
key = Array.isArray(key) ? key.join('.') : key;
this.compacted[key] = mutation;
},
/**
* Track a mutation.
*
* @param {String} instruction the type of mutation
* @param {...*} payload mutation parameters
*/
track (instruction, ...payload) {
// invalid call
if (!(instruction && payload)) {
return;
}
// unknown instruction
if (!(instruction === PRIMITIVE_MUTATIONS.SET || instruction === PRIMITIVE_MUTATIONS.UNSET)) {
return;
}
// for primitive mutations the arguments form the mutation object
// if there is more complex mutation, we have to use a processor to create a mutation for the instruction
this.addMutation(payload);
},
/**
* Compacts the recorded mutations removing duplicate mutations that apply on the same key path.
*/
compact () {
// for each of the mutation, add to compacted list
this.stream.forEach(this.addAndCompact.bind(this));
// reset the `stream`, all the mutations are now recorded in the `compacted` storage
this.stream = [];
},
/**
* Returns the number of mutations tracked so far.
*
* @returns {Number}
*/
count () {
// the total count of mutations is the sum of
// mutations in the stream
var mutationCount = this.stream.length;
// and the compacted mutations
mutationCount += Object.keys(this.compacted).length;
return mutationCount;
},
/**
* Applies all the recorded mutations on a target object.
*
* @param {*} target Target to apply mutations. Must implement `applyMutation`.
*/
applyOn (target) {
if (!(target && target.applyMutation)) {
return;
}
var applyIndividualMutation = function applyIndividualMutation (mutation) {
applyMutation(target, mutation);
};
// mutations move from `stream` to `compacted`, so we apply the compacted mutations first
// to ensure FIFO of mutations
// apply the compacted mutations first
_.forEach(this.compacted, applyIndividualMutation);
// apply the mutations in the stream
_.forEach(this.stream, applyIndividualMutation);
}
});
_.assign(MutationTracker, /** @lends MutationTracker */ {
/**
* Defines the name of this property for internal use.
*
* @private
* @readOnly
* @type {String}
*/
_postman_propertyName: 'MutationTracker',
/**
* Check whether an object is an instance of {@link MutationTracker}.
*
* @param {*} obj -
* @returns {Boolean}
*/
isMutationTracker: function (obj) {
return Boolean(obj) && ((obj instanceof MutationTracker) ||
_.inSuperChain(obj.constructor, '_postman_propertyName', MutationTracker._postman_propertyName));
}
});
module.exports = {
MutationTracker
};