@pact-foundation/pact
Version:
Pact for all things Javascript
544 lines • 18.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isStatusCodeMatcher = exports.matcherValueOrString = exports.equal = exports.contentType = exports.decimal = exports.integer = exports.boolean = exports.constrainedArrayLike = exports.atMostLike = exports.atLeastLike = exports.atLeastOneLike = exports.eachLike = exports.matchStatus = exports.eachValueMatches = exports.eachKeyMatches = exports.eachKeyLike = exports.like = void 0;
exports.isMatcher = isMatcher;
exports.number = number;
exports.string = string;
exports.regex = regex;
exports.datetime = datetime;
exports.timestamp = timestamp;
exports.time = time;
exports.date = date;
exports.includes = includes;
exports.nullValue = nullValue;
exports.url2 = url2;
exports.url = url;
exports.arrayContaining = arrayContaining;
exports.fromProviderState = fromProviderState;
exports.uuid = uuid;
exports.reify = reify;
exports.extractPayload = reify;
const ramda_1 = require("ramda");
const randexp_1 = __importDefault(require("randexp"));
__exportStar(require("./types"), exports);
function isMatcher(x) {
return (x != null &&
x['pact:matcher:type'] !== undefined &&
x.value !== undefined);
}
/**
* Value must match the given template
* @param template Template to base the comparison on
*/
const like = (template) => ({
'pact:matcher:type': 'type',
value: template,
});
exports.like = like;
/**
* Object where the key itself is ignored, but the value template must match.
*
* @deprecated use eachKeyMatches or eachValueMatches
* @param keyTemplate Example key to use
* @param template Example value template to base the comparison on
*/
const eachKeyLike = (keyTemplate, template) => ({
'pact:matcher:type': 'values',
value: {
[keyTemplate]: template,
},
});
exports.eachKeyLike = eachKeyLike;
/**
* Object where the _keys_ must match the supplied matchers.
* The values for each key are ignored. That is, there can be 0 or more keys
* with any valid JSON identifier, so long as the names of the keys match the constraints.
*
* @param example Example object with key/values e.g. `{ foo: 'bar', baz: 'qux'}`
* @param matchers Matchers to apply to each key
*/
const eachKeyMatches = (example, matchers = (0, exports.like)('key')) => ({
'pact:matcher:type': 'eachKey',
rules: Array.isArray(matchers) ? matchers : [matchers],
value: example,
});
exports.eachKeyMatches = eachKeyMatches;
/**
* Object where the _values_ must match the supplied matchers.
* The names of the keys are ignored. That is, there can be 0 or more keys
* with any valid JSON identifier, so long as the values match the constraints.
*
* @param example Example object with key/values e.g. `{ foo: 'bar', baz: 'qux'}`
* @param matchers Matchers to apply to each value
*/
const eachValueMatches = (example, matchers) => ({
'pact:matcher:type': 'eachValue',
rules: Array.isArray(matchers) ? matchers : [matchers],
value: example,
// Unsure if the full object is provided, or just a template k/v pair
// value: {
// [keyTemplate]: template,
// },
});
exports.eachValueMatches = eachValueMatches;
/**
* Matches HTTP status codes either by a class (e.g. 2XX) or a list of specific codes.
*
* @param example Example status code to use in consumer tests
* @param status Allowed status codes - either an HTTPResponseStatusClass (e.g. Success for 2XX)
* or an array of specific status codes (e.g. [200, 201])
*/
const matchStatus = (example, status) => ({
'pact:matcher:type': 'statusCode',
status,
value: example,
});
exports.matchStatus = matchStatus;
/**
* Array where each element must match the given template
* @param template Template to base the comparison on
* @param min Minimum number of elements required in the array
*/
const eachLike = (template, min = 1) => {
const elements = min;
return {
min,
'pact:matcher:type': 'type',
value: (0, ramda_1.times)(() => template, elements),
};
};
exports.eachLike = eachLike;
/**
* An array that has to have at least one element and each element must match the given template
* @param template Template to base the comparison on
* @param count Number of examples to generate, defaults to one
*/
const atLeastOneLike = (template, count = 1) => ({
min: 1,
'pact:matcher:type': 'type',
value: (0, ramda_1.times)(() => template, count),
});
exports.atLeastOneLike = atLeastOneLike;
/**
* An array that has to have at least the required number of elements and each element must match the given template
* @param template Template to base the comparison on
* @param min Minimum number of elements required in the array
* @param count Number of examples to generate, defaults to min
*/
const atLeastLike = (template, min, count) => {
const elements = count || min;
if (count && count < min) {
throw new Error(`atLeastLike has a minimum of ${min} but ${count} elements were requested.` +
` Make sure the count is greater than or equal to the min.`);
}
return {
min,
'pact:matcher:type': 'type',
value: (0, ramda_1.times)(() => template, elements),
};
};
exports.atLeastLike = atLeastLike;
/**
* An array that has to have at most the required number of elements and each element must match the given template
* @param template Template to base the comparison on
* @param max Maximum number of elements required in the array
* @param count Number of examples to generate, defaults to one
*/
const atMostLike = (template, max, count) => {
const elements = count || 1;
if (count && count > max) {
throw new Error(`atMostLike has a maximum of ${max} but ${count} elements where requested.` +
` Make sure the count is less than or equal to the max.`);
}
return {
max,
'pact:matcher:type': 'type',
value: (0, ramda_1.times)(() => template, elements),
};
};
exports.atMostLike = atMostLike;
/**
* An array whose size is constrained to the minimum and maximum number of elements and each element must match the given template
* @param template Template to base the comparison on
* @param min Minimum number of elements required in the array
* @param max Maximum number of elements required in the array
* @param count Number of examples to generate, defaults to one
*/
const constrainedArrayLike = (template, min, max, count) => {
const elements = count || min;
if (count) {
if (count < min) {
throw new Error(`constrainedArrayLike has a minimum of ${min} but ${count} elements where requested.` +
` Make sure the count is greater than or equal to the min.`);
}
else if (count > max) {
throw new Error(`constrainedArrayLike has a maximum of ${max} but ${count} elements where requested.` +
` Make sure the count is less than or equal to the max.`);
}
}
return {
min,
max,
'pact:matcher:type': 'type',
value: (0, ramda_1.times)(() => template, elements),
};
};
exports.constrainedArrayLike = constrainedArrayLike;
/**
* Value must be a boolean
* @param b Boolean example value. Defaults to true if unsupplied
*/
const boolean = (b = true) => ({
'pact:matcher:type': 'type',
value: b,
});
exports.boolean = boolean;
/**
* Value must be an integer (must be a number and have no decimal places)
* @param int Example value. If omitted a random value will be generated.
*/
const integer = (int) => {
if (Number.isInteger(int)) {
return {
'pact:matcher:type': 'integer',
value: int,
};
}
if (int) {
throw new Error(`The integer matcher was passed '${int}' which is not an integer.`);
}
return {
'pact:generator:type': 'RandomInt',
'pact:matcher:type': 'integer',
value: 101,
};
};
exports.integer = integer;
/**
* Value must be a decimal number (must be a number and have decimal places)
* @param num Example value. If omitted a random value will be generated.
*/
const decimal = (num) => {
if (Number.isFinite(num)) {
return {
'pact:matcher:type': 'decimal',
value: num,
};
}
if (num) {
throw new Error(`The decimal matcher was passed '${num}' which is not a number.`);
}
return {
'pact:generator:type': 'RandomDecimal',
'pact:matcher:type': 'decimal',
value: 12.34,
};
};
exports.decimal = decimal;
/**
* Value must be a number
* @param num Example value. If omitted a random integer value will be generated.
*/
function number(num) {
if (typeof num === 'number') {
return {
'pact:matcher:type': 'number',
value: num,
};
}
if (num) {
throw new Error(`The number matcher was passed '${num}' which is not a number.`);
}
return {
'pact:generator:type': 'RandomInt',
'pact:matcher:type': 'number',
value: 1234,
};
}
/**
* Value must be a string
* @param str Example value
*/
function string(str = 'some string') {
return {
'pact:matcher:type': 'type',
value: str,
};
}
/**
* Value that must match the given regular expression
* @param pattern Regular Expression to match
* @param str Example value
*/
function regex(pattern, str) {
if (pattern instanceof RegExp) {
return {
'pact:matcher:type': 'regex',
regex: pattern.source,
value: str,
};
}
return {
'pact:matcher:type': 'regex',
regex: pattern,
value: str,
};
}
/**
* Matches the content type of a multipart field.
* Used for matching binary content or specific content types in multipart requests.
* @param contentTypeValue The content type to match (e.g., 'image/jpeg', 'text/plain')
*/
const contentType = (contentTypeValue) => ({
'pact:matcher:type': 'contentType',
value: contentTypeValue,
});
exports.contentType = contentType;
/**
* Value that must be equal to the example. This is mainly used to reset the matching rules which cascade.
* @param value Example value
*/
const equal = (value) => ({
'pact:matcher:type': 'equality',
value,
});
exports.equal = equal;
/**
* String value that must match the provided datetime format string.
* @param format Datetime format string. See [Java SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
* @param example Example value to use. If omitted a value using the current system date and time will be generated.
*/
function datetime(format, example) {
if (!example) {
throw new Error(`you must provide an example datetime`);
}
return (0, ramda_1.pickBy)((v) => !(0, ramda_1.isNil)(v), {
'pact:generator:type': example ? undefined : 'DateTime',
'pact:matcher:type': 'timestamp',
format,
value: example,
});
}
/**
* String value that must match the provided datetime format string.
* @param format Datetime format string. See [Java SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
* @param example Example value to use. If omitted a value using the current system date and time will be generated.
*/
function timestamp(format, example) {
if (!example) {
throw new Error(`you must provide an example timestamp`);
}
return datetime(format, example);
}
/**
* String value that must match the provided time format string.
* @param format Time format string. See [Java SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
* @param example Example value to use. If omitted a value using the current system time will be generated.
*/
function time(format, example) {
if (!example) {
throw new Error(`you must provide an example time`);
}
return (0, ramda_1.pickBy)((v) => !(0, ramda_1.isNil)(v), {
'pact:generator:type': example ? undefined : 'Time',
'pact:matcher:type': 'time',
format,
value: example,
});
}
/**
* String value that must match the provided date format string.
* @param format Date format string. See [Java SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
* @param example Example value to use. If omitted a value using the current system date will be generated.
*/
function date(format, example) {
if (!example) {
throw new Error(`you must provide an example date`);
}
return (0, ramda_1.pickBy)((v) => !(0, ramda_1.isNil)(v), {
format,
'pact:generator:type': example ? undefined : 'Date',
'pact:matcher:type': 'date',
value: example,
});
}
/**
* Value that must include the example value as a substring.
* @param value String value to include
*/
function includes(value) {
return {
'pact:matcher:type': 'include',
value,
};
}
/**
* Value that must be null. This will only match the JSON Null value. For other content types, it will
* match if the attribute is missing.
*/
function nullValue() {
return {
'pact:matcher:type': 'null',
value: null,
};
}
function stringFromRegex(r) {
return new randexp_1.default(r).gen();
}
/**
* Matches a URL composed of a base path and a list of path fragments
* @param basePath Base path of the URL. If null, will use the base URL from the mock server.
* @param pathFragments list of path fragments, can be regular expressions
*/
function url2(basePath, pathFragments) {
const regexpr = [
'.*(',
...pathFragments.map((p) => {
if (p instanceof RegExp) {
return `\\/${p.source}`;
}
if (p instanceof Object && p['pact:matcher:type'] === 'regex') {
return `\\/${p.regex}`;
}
return `\\/${p.toString()}`;
}),
].join('');
const example = [
basePath || 'http://localhost:8080',
...pathFragments.map((p) => {
if (p instanceof RegExp) {
return `/${stringFromRegex(p)}`;
}
if (p instanceof Object && p['pact:matcher:type'] === 'regex') {
return `/${p.value}`;
}
return `/${p.toString()}`;
}),
].join('');
// Temporary fix for inconsistencies between matchers and generators. Matchers use "value" attribute for
// example values, while generators use "example"
if (basePath == null) {
return {
'pact:matcher:type': 'regex',
'pact:generator:type': 'MockServerURL',
regex: `${regexpr})$`,
value: example,
example,
};
}
return {
'pact:matcher:type': 'regex',
regex: `${regexpr})$`,
value: example,
};
}
/**
* Matches a URL composed of a list of path fragments. The base URL from the mock server will be used.
* @param pathFragments list of path fragments, can be regular expressions
*/
function url(pathFragments) {
return url2(null, pathFragments);
}
/**
* Matches the items in an array against a number of variants. Matching is successful if each variant
* occurs once in the array. Variants may be objects containing matching rules.
*/
function arrayContaining(...variants) {
return {
'pact:matcher:type': 'arrayContains',
variants,
};
}
/**
* Marks an item to be injected from the provider state
* @param expression Expression to lookup in the provider state context
* @param exampleValue Example value to use in the consumer test
*/
function fromProviderState(expression, exampleValue) {
return {
'pact:matcher:type': 'type',
'pact:generator:type': 'ProviderState',
expression,
value: exampleValue,
};
}
/**
* Match a universally unique identifier (UUID). Random values will be used for examples if no example is given.
*/
function uuid(example) {
const regexStr = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
if (example) {
const regexpr = new RegExp(`^${regexStr}$`);
if (!example.match(regexpr)) {
throw new Error(`regex: Example value '${example}' does not match the UUID regular expression '${regexStr}'`);
}
return {
'pact:matcher:type': 'regex',
regex: regexStr,
value: example,
};
}
return {
'pact:matcher:type': 'regex',
regex: regexStr,
'pact:generator:type': 'Uuid',
value: 'e2490de5-5bd3-43d5-b7c4-526e33f71304',
};
}
const matcherValueOrString = (obj) => {
if (typeof obj === 'string')
return obj;
return JSON.stringify(obj);
};
exports.matcherValueOrString = matcherValueOrString;
/**
* Type guard to check if a value is a StatusCodeMatcher.
*/
const isStatusCodeMatcher = (status) => isMatcher(status) && status['pact:matcher:type'] === 'statusCode';
exports.isStatusCodeMatcher = isStatusCodeMatcher;
/**
* Recurse the object removing any underlying matching guff, returning the raw
* example content.
*/
function reify(input) {
if (isMatcher(input)) {
return reify(input.value);
}
if (Array.isArray(input)) {
return input.map(reify);
}
if (typeof input === 'object') {
if (input === null) {
return input;
}
const objectInput = input;
return Object.keys(objectInput).reduce((acc, propName) => {
acc[propName] = reify(objectInput[propName]);
return acc;
}, {});
}
if (typeof input === 'number' ||
typeof input === 'string' ||
typeof input === 'boolean') {
return input;
}
throw new Error(`Unable to strip matcher from a '${typeof input}', as it is not valid in a Pact description`);
}
//# sourceMappingURL=matchers.js.map