UNPKG

@pact-foundation/pact

Version:
544 lines 18.8 kB
"use strict"; 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