var _ = require('../util').lodash,
__PARENT = '__parent',
PropertyBase; // constructor
/**
* @typedef PropertyBase.definition
* @property {String|Description} [description]
*/
/**
* Base of all properties in Postman Collection. It defines the root for all standalone properties for postman
* collection.
*
* @constructor
* @param {PropertyBase.definition} definition -
*/
PropertyBase = function PropertyBase (definition) {
// In case definition object is missing, there is no point moving forward. Also if the definition is basic string
// we do not need to do anything with it.
if (!definition || typeof definition === 'string') { return; }
// call the meta extraction functions to create the object where all keys that are prefixed with underscore can be
// stored. more details on that can be retrieved from the propertyExtractMeta function itself.
// @todo: make this a closed function to do getter and setter which is non enumerable
var src = definition && definition.info || definition,
meta = _(src).pickBy(PropertyBase.propertyIsMeta).mapKeys(PropertyBase.propertyUnprefixMeta).value();
if (_.keys(meta).length) {
this._ = _.isObject(this._) ?
/* istanbul ignore next */
_.mergeDefined(this._, meta) :
meta;
}
};
_.assign(PropertyBase.prototype, /** @lends PropertyBase.prototype */ {
/**
* Invokes the given iterator for every parent in the parent chain of the given element.
*
* @param {Object} options - A set of options for the parent chain traversal.
* @param {?Boolean} [options.withRoot=false] - Set to true to include the collection object as well.
* @param {Function} iterator - The function to call for every parent in the ancestry chain.
* @todo Cache the results
*/
forEachParent (options, iterator) {
_.isFunction(options) && (iterator = options, options = {});
if (!_.isFunction(iterator) || !_.isObject(options)) { return; }
var parent = this.parent(),
grandparent = parent && _.isFunction(parent.parent) && parent.parent();
while (parent && (grandparent || options.withRoot)) {
iterator(parent);
parent = grandparent;
grandparent = grandparent && _.isFunction(grandparent.parent) && grandparent.parent();
}
},
/**
* Tries to find the given property locally, and then proceeds to lookup in each parent,
* going up the chain as necessary. Lookup will continue until `customizer` returns a truthy value. If used
* without a customizer, the lookup will stop at the first parent that contains the property.
*
* @param {String} property -
* @param {Function} [customizer] -
* @returns {*|undefined}
*/
findInParents (property, customizer) {
var owner = this.findParentContaining(property, customizer);
return owner ? owner[property] : undefined;
},
/**
* Looks up the closest parent which has a truthy value for the given property. Lookup will continue
* until `customizer` returns a truthy value. If used without a customizer,
* the lookup will stop at the first parent that contains the property.
*
* @private
* @param {String} property -
* @param {Function} [customizer] -
* @returns {PropertyBase|undefined}
*/
findParentContaining (property, customizer) {
var parent = this;
// if customizer is present test with it
if (customizer) {
customizer = customizer.bind(this);
do {
// else check for existence
if (customizer(parent)) {
return parent;
}
parent = parent.__parent;
} while (parent);
}
// else check for existence
else {
do {
if (parent[property]) {
return parent;
}
parent = parent.__parent;
} while (parent);
}
},
/**
* Returns the JSON representation of a property, which conforms to the way it is defined in a collection.
* You can use this method to get the instantaneous representation of any property, including a {@link Collection}.
*/
toJSON () {
return _.reduce(this, function (accumulator, value, key) {
if (value === undefined) { // true/false/null need to be preserved.
return accumulator;
}
// Handle plurality of PropertyLists in the SDK vs the exported JSON.
// Basically, removes the trailing "s" from key if the value is a property list.
// eslint-disable-next-line max-len
if (value && value._postman_propertyIsList && !value._postman_proprtyIsSerialisedAsPlural && _.endsWith(key, 's')) {
key = key.slice(0, -1);
}
// Handle 'PropertyBase's
if (value && _.isFunction(value.toJSON)) {
accumulator[key] = value.toJSON();
return accumulator;
}
// Handle Strings
if (_.isString(value)) {
accumulator[key] = value;
return accumulator;
}
// Everything else
accumulator[key] = _.cloneElement(value);
return accumulator;
}, {});
},
/**
* Returns the meta keys associated with the property
*
* @returns {*}
*/
meta () {
return arguments.length ? _.pick(this._, Array.prototype.slice.apply(arguments)) : _.cloneDeep(this._);
},
/**
* Returns the parent of item
*
* @returns {*|undefined}
*/
parent () {
// @todo return grandparent only if it is a list
return this && this.__parent && (this.__parent.__parent || this.__parent) || undefined;
},
/**
* Accepts an object and sets it as the parent of the current property.
*
* @param {Object} parent The object to set as parent.
* @private
*/
setParent (parent) {
_.assignHidden(this, __PARENT, parent);
}
});
_.assign(PropertyBase, /** @lends PropertyBase */ {
/**
* Defines the name of this property for internal use.
*
* @private
* @readOnly
* @type {String}
*/
_postman_propertyName: 'PropertyBase',
/**
* Filter function to check whether a key starts with underscore or not. These usually are the meta properties. It
* returns `true` if the criteria is matched.
*
* @param {*} value -
* @param {String} key -
*
* @returns {boolean}
*/
propertyIsMeta: function (value, key) {
return _.startsWith(key, '_') && (key !== '_');
},
/**
* Map function that removes the underscore prefix from an object key.
*
* @param {*} value -
* @param {String} key -
* @returns {String}
*/
propertyUnprefixMeta: function (value, key) {
return _.trimStart(key, '_');
},
/**
* Static function which allows calling toJSON() on any object.
*
* @param {Object} obj -
* @returns {*}
*/
toJSON: function (obj) {
return PropertyBase.prototype.toJSON.call(obj);
}
});
module.exports = {
PropertyBase
};