var _ = require('../util').lodash, uuid = require('uuid'), PropertyBase = require('./property-base').PropertyBase, Description = require('./description').Description, Substitutor = require('../superstring').Substitutor, DISABLED = 'disabled', DESCRIPTION = 'description', REGEX_EXTRACT_VARS = Substitutor.REGEX_EXTRACT_VARS, Property; // constructor /** * Recursively traverses a variable and detects all instances of variable * replacements within the string of the object. * * @private * @param {*} value Any JS variable within which we are trying to discover {{variables}} * @param {[Object]} seen Set of objects traversed before to avoid infinite recursion * @param {[Object]} result Set of variables to accumulate result in the recursive call * @returns {Object} Set of variables */ function _findSubstitutions (value, seen = new Set(), result = new Set()) { if (!value || seen.has(value)) { return result; } if (Array.isArray(value)) { seen.add(value); for (let i = 0, ii = value.length; i < ii; i++) { _findSubstitutions(value[i], seen, result); } } else if (typeof value === 'object') { seen.add(value); for (const key in value) { if (Object.hasOwnProperty.call(value, key)) { _findSubstitutions(value[key], seen, result); } } } else if (typeof value === 'string') { let match; while ((match = REGEX_EXTRACT_VARS.exec(value)) !== null) { result.add(match[0].slice(2, -2)); } } return result; } /** * @typedef Property.definition * @property {String=} [id] A unique string that identifies the property. * @property {String=} [name] A distinctive and human-readable name of the property. * @property {Boolean=} [disabled] Denotes whether the property is disabled or not. * @property {Object=} [info] The meta information regarding the Property is provided as the `info` object. * @property {String=} [info.id] If set, this is used instead of the definition root's id. * @property {String=} [info.name] If set, this is used instead of the definition root's name. */ _.inherit(( /** * The Property class forms the base of all postman collection SDK elements. This is to be used only for SDK * development or to extend the SDK with additional functionalities. All SDK classes (constructors) that are * supposed to be identifyable (i.e. ones that can have a `name` and `id`) are derived from this class. * * For more information on what is the structure of the `definition` the function parameter, have a look at * {@link Property.definition}. * * > This is intended to be a private class except for those who want to extend the SDK itself and add more * > functionalities. * * @constructor * @extends {PropertyBase} * * @param {Property.definition=} [definition] Every constructor inherited from `Property` is required to call the * super constructor function. This implies that construction parameters of every inherited member is propagated * to be sent up to this point. * * @see Property.definition */ Property = function PostmanProperty (definition) { // this constructor is intended to inherit and as such the super constructor is required to be executed Property.super_.apply(this, arguments); // The definition can have an `info` object that stores the identification of this property. If that is present, // we use it instead of the definition root. var src = definition && definition.info || definition, id; // first we extract id from all possible sources // we also check if this property is marked to require an ID, we generate one if not found. id = (src && src.id) || this.id || (this._ && this._.postman_id) || (this._postman_propertyRequiresId && uuid.v4()); /** * The `id` of the property is a unique string that identifies this property and can be used to refer to * this property from relevant other places. It is a good practice to define the id or let the system * auto generate a UUID if one is not defined for properties that require an `id`. * * @name id * @type {String} * @memberOf Property.prototype * * @note The property can also be present in the `postman_id` meta in case it is not specified in the * object. An auto-generated property is used wherever one is not specified */ id && (this.id = id); /** * A property can have a distinctive and human-readable name. This is to be used to display the name of the * property within Postman, Newman or other runtimes that consume collection. In certain cases, the absence * of name might cause the runtime to use the `id` as a fallback. * * @name name * @memberOf Property.prototype * @type {String} */ src && src.name && (this.name = src.name); /** * This (optional) flag denotes whether this property is disabled or not. Usually, this is helpful when a * property is part of a {@link PropertyList}. For example, in a PropertyList of {@link Header}s, the ones * that are disabled can be filtered out and not processed. * * @type {Boolean} * @optional * @name disabled * * @memberOf Property.prototype */ definition && _.has(definition, DISABLED) && (this.disabled = Boolean(definition.disabled)); /** * The `description` property holds the detailed documentation of any property. * It is recommended that this property be updated using the [describe](#describe) function. * * @type {Description} * @see Property#describe * * @example Accessing descriptions of all root items in a collection * var fs = require('fs'), // needed to read JSON file from disk * Collection = require('postman-collection').Collection, * myCollection; * * // Load a collection to memory from a JSON file on disk (say, sample-collection.json) * myCollection = new Collection(JSON.stringify(fs.readFileSync('sample-collection.json').toString())); * * // Log the description of all root items * myCollection.item.all().forEach(function (item) { * console.log(item.name || 'Untitled Item'); * item.description && console.log(item.description.toString()); * }); */ // eslint-disable-next-line max-len _.has(src, DESCRIPTION) && (this.description = _.createDefined(src, DESCRIPTION, Description, this.description)); }), PropertyBase); _.assign(Property.prototype, /** @lends Property.prototype */ { /** * This function allows to describe the property for the purpose of detailed identification or documentation * generation. This function sets or updates the `description` child-property of this property. * * @param {String} content The content of the description can be provided here as a string. Note that it is expected * that if the content is formatted in any other way than simple text, it should be specified in the subsequent * `type` parameter. * @param {String=} [type="text/plain"] The type of the content. * * @example Add a description to an instance of Collection * var Collection = require('postman-collection').Collection, * mycollection; * * // create a blank collection * myCollection = new Collection(); * myCollection.describe('Hey! This is a cool collection.'); * * console.log(myCollection.description.toString()); // read the description */ describe (content, type) { (Description.isDescription(this.description) ? this.description : (this.description = new Description())) .update(content, type); }, /** * Returns an object representation of the Property with its variable references substituted. * * @example Resolve an object using variable definitions from itself and its parents * property.toObjectResolved(); * * @example Resolve an object using variable definitions on a different object * property.toObjectResolved(item); * * @example Resolve an object using variables definitions as a flat list of variables * property.toObjectResolved(null, [variablesDefinition1, variablesDefinition1], {ignoreOwnVariables: true}); * * @private * @draft * @param {?Item|ItemGroup=} [scope] - One can specifically provide an item or group with `.variables`. In * the event one is not provided, the variables are taken from this object or one from the parent tree. * @param {Array} overrides - additional objects to lookup for variable values * @param {Object} [options] - * @param {Boolean} [options.ignoreOwnVariables] - if set to true, `.variables` on self(or scope) * will not be used for variable resolution. Only variables in `overrides` will be used for resolution. * @returns {Object|undefined} * @throws {Error} If `variables` cannot be resolved up the parent chain. */ toObjectResolved (scope, overrides, options) { var ignoreOwnVariables = options && options.ignoreOwnVariables, variableSourceObj, variables, reference; // ensure you do not substitute variables itself! reference = this.toJSON(); _.isArray(reference.variable) && (delete reference.variable); // if `ignoreScopeVariables` is turned on, ignore `.variables` and resolve with only `overrides` // otherwise find `.variables` on current object or `scope` if (ignoreOwnVariables) { return Property.replaceSubstitutionsIn(reference, overrides); } // 1. if variables is passed as params, use it or fall back to oneself // 2. for a source from point (1), and look for `.variables` // 3. if `.variables` is not found, then rise up the parent to find first .variables variableSourceObj = scope || this; do { variables = variableSourceObj.variables; variableSourceObj = variableSourceObj.__parent; } while (!variables && variableSourceObj); if (!variables) { // worst case = no variable param and none detected in tree or object throw Error('Unable to resolve variables. Require a List type property for variable auto resolution.'); } return variables.substitute(reference, overrides); }, /** * Returns all the substitutions (variables) that are needed (or referenced) in this property (recursively). * * @returns {String[]} * * @example * // returns ['host', 'path1'] * prop.findSubstitutions({request: 'https://{{host}}/{{path1}}-action/'}); * * @see {Property.findSubstitutions} */ findSubstitutions () { return Property.findSubstitutions(this.toJSON()); } }); _.assign(Property, /** @lends Property */ { /** * Defines the name of this property for internal use. * * @private * @readOnly * @type {String} */ _postman_propertyName: 'Property', /** * This function accepts a string followed by a number of variable sources as arguments. One or more variable * sources can be provided and it will use the one that has the value in left-to-right order. * * @param {String} str - * @param {VariableList|Object|Array.} variables - * @returns {String} */ // @todo: improve algorithm via variable replacement caching replaceSubstitutions: function (str, variables) { // if there is nothing to replace, we move on if (!(str && _.isString(str))) { return str; } // if variables object is not an instance of substitutor then ensure that it is an array so that it becomes // compatible with the constructor arguments for a substitutor !Substitutor.isInstance(variables) && !_.isArray(variables) && (variables = _.tail(arguments)); return Substitutor.box(variables, Substitutor.DEFAULT_VARS).parse(str).toString(); }, /** * This function accepts an object followed by a number of variable sources as arguments. One or more variable * sources can be provided and it will use the one that has the value in left-to-right order. * * @param {Object} obj - * @param {Array.} variables - * @returns {Object} */ replaceSubstitutionsIn: function (obj, variables) { // if there is nothing to replace, we move on if (!(obj && _.isObject(obj))) { return obj; } // convert the variables to a substitutor object (will not reconvert if already substitutor) variables = Substitutor.box(variables, Substitutor.DEFAULT_VARS); var customizer = function (objectValue, sourceValue) { objectValue = objectValue || {}; if (!_.isString(sourceValue)) { _.forOwn(sourceValue, function (value, key) { sourceValue[key] = customizer(objectValue[key], value); }); return sourceValue; } return this.replaceSubstitutions(sourceValue, variables); }.bind(this); return _.mergeWith({}, obj, customizer); }, /** * This function recursively traverses a variable and detects all instances of variable replacements * within the string of the object * * @param {*} obj Any JS variable within which we are trying to discover {{variables}} * @returns {String[]} * * @example * // returns ['host', 'path1'] * Property.findSubstitutions({request: 'https://{{host}}/{{path1}}-action/'}); * * @todo * - tonne of scope for performance optimizations * - accept a reference variable scope so that substitutions can be applied to find recursive * replacements (e.g. {{hello-{{hi}}-var}}) */ findSubstitutions: function (obj) { return Array.from(_findSubstitutions(obj)); } }); module.exports = { Property };