Path: blob/master/node_modules/@hapi/hoek/lib/contain.js
1126 views
'use strict';12const Assert = require('./assert');3const DeepEqual = require('./deepEqual');4const EscapeRegex = require('./escapeRegex');5const Utils = require('./utils');678const internals = {};91011module.exports = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols }1213/*14string -> string(s)15array -> item(s)16object -> key(s)17object -> object (key:value)18*/1920if (typeof values !== 'object') {21values = [values];22}2324Assert(!Array.isArray(values) || values.length, 'Values array cannot be empty');2526// String2728if (typeof ref === 'string') {29return internals.string(ref, values, options);30}3132// Array3334if (Array.isArray(ref)) {35return internals.array(ref, values, options);36}3738// Object3940Assert(typeof ref === 'object', 'Reference must be string or an object');41return internals.object(ref, values, options);42};434445internals.array = function (ref, values, options) {4647if (!Array.isArray(values)) {48values = [values];49}5051if (!ref.length) {52return false;53}5455if (options.only &&56options.once &&57ref.length !== values.length) {5859return false;60}6162let compare;6364// Map values6566const map = new Map();67for (const value of values) {68if (!options.deep ||69!value ||70typeof value !== 'object') {7172const existing = map.get(value);73if (existing) {74++existing.allowed;75}76else {77map.set(value, { allowed: 1, hits: 0 });78}79}80else {81compare = compare || internals.compare(options);8283let found = false;84for (const [key, existing] of map.entries()) {85if (compare(key, value)) {86++existing.allowed;87found = true;88break;89}90}9192if (!found) {93map.set(value, { allowed: 1, hits: 0 });94}95}96}9798// Lookup values99100let hits = 0;101for (const item of ref) {102let match;103if (!options.deep ||104!item ||105typeof item !== 'object') {106107match = map.get(item);108}109else {110compare = compare || internals.compare(options);111112for (const [key, existing] of map.entries()) {113if (compare(key, item)) {114match = existing;115break;116}117}118}119120if (match) {121++match.hits;122++hits;123124if (options.once &&125match.hits > match.allowed) {126127return false;128}129}130}131132// Validate results133134if (options.only &&135hits !== ref.length) {136137return false;138}139140for (const match of map.values()) {141if (match.hits === match.allowed) {142continue;143}144145if (match.hits < match.allowed &&146!options.part) {147148return false;149}150}151152return !!hits;153};154155156internals.object = function (ref, values, options) {157158Assert(options.once === undefined, 'Cannot use option once with object');159160const keys = Utils.keys(ref, options);161if (!keys.length) {162return false;163}164165// Keys list166167if (Array.isArray(values)) {168return internals.array(keys, values, options);169}170171// Key value pairs172173const symbols = Object.getOwnPropertySymbols(values).filter((sym) => values.propertyIsEnumerable(sym));174const targets = [...Object.keys(values), ...symbols];175176const compare = internals.compare(options);177const set = new Set(targets);178179for (const key of keys) {180if (!set.has(key)) {181if (options.only) {182return false;183}184185continue;186}187188if (!compare(values[key], ref[key])) {189return false;190}191192set.delete(key);193}194195if (set.size) {196return options.part ? set.size < targets.length : false;197}198199return true;200};201202203internals.string = function (ref, values, options) {204205// Empty string206207if (ref === '') {208return values.length === 1 && values[0] === '' || // '' contains ''209!options.once && !values.some((v) => v !== ''); // '' contains multiple '' if !once210}211212// Map values213214const map = new Map();215const patterns = [];216217for (const value of values) {218Assert(typeof value === 'string', 'Cannot compare string reference to non-string value');219220if (value) {221const existing = map.get(value);222if (existing) {223++existing.allowed;224}225else {226map.set(value, { allowed: 1, hits: 0 });227patterns.push(EscapeRegex(value));228}229}230else if (options.once ||231options.only) {232233return false;234}235}236237if (!patterns.length) { // Non-empty string contains unlimited empty string238return true;239}240241// Match patterns242243const regex = new RegExp(`(${patterns.join('|')})`, 'g');244const leftovers = ref.replace(regex, ($0, $1) => {245246++map.get($1).hits;247return ''; // Remove from string248});249250// Validate results251252if (options.only &&253leftovers) {254255return false;256}257258let any = false;259for (const match of map.values()) {260if (match.hits) {261any = true;262}263264if (match.hits === match.allowed) {265continue;266}267268if (match.hits < match.allowed &&269!options.part) {270271return false;272}273274// match.hits > match.allowed275276if (options.once) {277return false;278}279}280281return !!any;282};283284285internals.compare = function (options) {286287if (!options.deep) {288return internals.shallow;289}290291const hasOnly = options.only !== undefined;292const hasPart = options.part !== undefined;293294const flags = {295prototype: hasOnly ? options.only : hasPart ? !options.part : false,296part: hasOnly ? !options.only : hasPart ? options.part : false297};298299return (a, b) => DeepEqual(a, b, flags);300};301302303internals.shallow = function (a, b) {304305return a === b;306};307308309