346 lines
14 KiB
JavaScript
346 lines
14 KiB
JavaScript
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 <caption>Accessing descriptions of all root items in a collection</caption>
|
|
* 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 <caption>Add a description to an instance of Collection</caption>
|
|
* 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 <caption>Resolve an object using variable definitions from itself and its parents</caption>
|
|
* property.toObjectResolved();
|
|
*
|
|
* @example <caption>Resolve an object using variable definitions on a different object</caption>
|
|
* property.toObjectResolved(item);
|
|
*
|
|
* @example <caption>Resolve an object using variables definitions as a flat list of variables</caption>
|
|
* 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<Object>} 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.<VariableList|Object>} 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.<VariableList|Object>} 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
|
|
};
|