UNPKG

18.6 kBJavaScriptView Raw
1/*jslint node: true */
2'use strict';
3var async = require('async');
4
5var constants = {
6 PKG_NAME: "apom",
7};
8
9// "pObj" => the Pattern object. The object that will have RegExp, if any
10// Usually the left object (1st parameter) in function calls
11// "tObj" => the Target object. The object that will be tested against.
12// Usually the right object (2nd parameter) in function calls
13// eg itMatches({cat: /gr.y/}, {cat: "gray"}) => true
14// pObj tObj
15
16var _ = {
17 isObjClass : function(obj, clas) {
18 return Object.prototype.toString.call(obj) === clas;
19 },
20 isObject: function(obj) {
21 // returns true for anything except javascript primitive
22 return obj instanceof Object;
23 },
24 isObjectLiteral: function(obj) {
25 return this.isObjClass(obj, "[object Object]");
26 },
27 isString: function(obj) {
28 return this.isObjClass(obj, "[object String]");
29 },
30 isUndefined: function(obj) {
31 return this.isObjClass(obj, "[object Undefined]");
32 },
33 isArray: function(obj) {
34 return this.isObjClass(obj, "[object Array]");
35 },
36 isRegExp: function(obj) {
37 return this.isObjClass(obj, "[object RegExp]");
38 },
39 isNull: function(obj) {
40 return this.isObjClass(obj, "[object Null]");
41 },
42 isFunction: function(obj) {
43 return this.isObjClass(obj, "[object Function]");
44 }
45};
46
47var optionsDefault = {
48 regExpMatch: false, // match on regular expression, pObj converted to Reg Exp object
49 regExpIgnoreCase: false, // ignore case upper/lower on reg exp match
50 regExpAnchorStart: false, // append "^" to beginning of prop value string
51 regExpAnchorEnd: false, // append "$" to end of prop value string
52
53 matchIfPObjPropMissing: false, // match on this property if doesn't exist
54 matchIfTObjPropMissing: false, // match on this property if doesn't exist
55
56 variablesAllowed: false, // substitute variables in prop value
57 getVariables: undefined, // function to call to get variables
58 // eg; function(cb) {return cb(null, {youngDog: "puppy"}); }
59 variablesStartStr: '~', // string in prop value to find the variable name
60 variablesEndStr: null, // string in prop value to find the variable name
61
62 propMatchFn: null // function to call instead of standard propMatch function
63};
64
65function getObjectProperties(obj) {
66 // returns a list of all properties, all levels, of the object as an
67 // array in dot notation; eg; ["prop1"."cat", "prop1"."dog"]
68 var props = [];
69 objectPropertyMap(
70 obj,
71 function(err, propName, propValue, propFullPath) {
72 props.push(propFullPath);
73 }
74 );
75
76 return props;
77}
78
79function makeMatchFn(props, options) {
80
81 var propMatchFunctions = getPropMatchFns(props, options);
82
83 return function(pObj, tObj, cb) {
84 return matches(pObj, tObj, propMatchFunctions, cb);
85 };
86}
87
88function getPropMatchFns(props, options) {
89 // returns an array of property match functions using
90 // props array (see getPropDefns) and default property definition
91 var propMatchFunctions = [];
92
93 getPropDefns(props, options)
94 .forEach(function(propDefn) {
95 makePropMatchFn(propDefn, options, function(err, fn){
96 propMatchFunctions.push(fn);
97 });
98 });
99 return propMatchFunctions;
100}
101
102function matches(pObj, tObj, propMatchFunctions, cb) {
103 // propMatchFunctions is an array of functions, typically one for each
104 // property to be tested for a match. See makeMatchFn and makePropMatchFn.
105 // If all return true it's a match
106
107 if (_.isFunction(propMatchFunctions)) {
108 cb = propMatchFunctions;
109 propMatchFunctions =
110 getPropMatchFns(getObjectProperties(pObj), {});
111 }
112
113 return async.every(
114 propMatchFunctions,
115 function(propMatchFunction, cb) {
116 propMatchFunction(pObj, tObj, cb);
117 },
118 function(result) {
119 // propMatchFunctions = null;
120 return cb(result);
121 }
122 );
123}
124
125function setOptionsDefault(options, optionsDefault) {
126 //if a property doesn't exist on options object, that property is set to
127 // the value of that property on optionsDefault
128 //Allows an options object to be passed in with only those properties that
129 // are wanted to over-ride the default option properties
130 if (options) {
131 var optionsDefaultKeys = Object.keys(optionsDefault);
132 // set each property on options to default for those that aren't set; eg,
133 // options.checkMethod = options.checkMethod || optionsDefault.checkMethod
134 for (var i = optionsDefaultKeys.length - 1; i >= 0; i--) {
135 options[optionsDefaultKeys[i]] =
136 options.hasOwnProperty(optionsDefaultKeys[i]) ?
137 options[optionsDefaultKeys[i]] :
138 optionsDefault[optionsDefaultKeys[i]];
139 }
140 } else {
141 options = optionsDefault;
142 }
143
144 return options;
145}
146
147function getPropDefns(props, options) {
148 // propDefns is an array of the properties and their options to be tested
149 // converting props parameter to a consistent form
150 // that can be passed to makePropMatchFn:
151 // [{name: 'propName', optionKey1: optionValue1,...},...]
152 // setting an options object on each property, using the options values
153 // set on each property, if any, first and then any options values
154 // that are passed in here for the overall options object and finally
155 // resolving to options default if neither are set
156 //
157 // "props" are the object properties to be compared.
158 // The "props" parameter can be
159 // 1. an array of strings which are the property names
160 // 2. an array of propDefn objects; name is required
161 // 3. an array of combination of string prop names and propDefn objects
162 // 4. an object-literal of property names as the key
163 // with propDefn objects
164 // eg: ["myProperty"]
165 // [{name: "myProperty", regExpMatch: false}]
166 // [{name: "myProperty", regExpMatch: false}, "myOtherProperty"]
167 // {myProperty: {regExpMatch: false}}
168
169 var propDefns;
170
171 // set each option property to default value if not set in options paramater
172 options = setOptionsDefault(options, optionsDefault);
173
174 // if props is an object-literal, convert to an array of objects
175 if(_.isObjectLiteral(props)) {
176 var propNames = Object.keys(props);
177 propDefns = [];
178 for (var i = propNames.length - 1; i >= 0; i--) {
179 // add name to object if doesn't exist
180 props[propNames[i]].name = props[propNames[i]].name || propNames[i];
181 propDefns.push(props[propNames[i]]);
182 }
183 } else {
184 propDefns = props;
185 }
186
187 // assign default values to each prop defn where it isn't already assigned
188 for (var j = propDefns.length - 1; j >= 0; j--) {
189 if(_.isString(propDefns[j])) {
190 var propDefn = {};
191 propDefn.name = propDefns[j];
192 propDefn = setOptionsDefault(propDefn, options);
193 propDefns[j] = propDefn;
194 } else if(_.isObjectLiteral(propDefns[j]) &&
195 _.isString(propDefns[j].name)) {
196 propDefns[j] = setOptionsDefault(propDefns[j], options);
197 } else {
198 throw new Error("Invalid parameter of properties; must be either an array" +
199 " of poperty names or an object-literal with property names as keys");
200 }
201 }
202
203 return propDefns;
204}
205
206function makePropMatchFn(propDefn, options, callback) {
207 // this creates and returns the match function for one property;
208 // that is the match function that is called for this one property
209 // to determine if it is a match (true) or not (false)
210
211 // use propDefn.propMatchFn if it is defined in options by user,
212 // otherwise use the standard match function
213 var matchFn = _.isNull(propDefn.propMatchFn) ?
214 makeStandardPropMatchFn(propDefn, options) :
215 propDefn.propMatchFn;
216
217 function returnFn(pObj, tObj, cb) {
218
219 getPropRefs(
220 pObj,
221 tObj,
222 propDefn.name,
223 function(err, propRefsObj){
224 matchFn(
225 propRefsObj.pObjProp,
226 propRefsObj.tObjProp,
227 cb);
228 });
229
230 }
231
232 return callback(null, returnFn);
233}
234
235function makeStandardPropMatchFn(propDefn, options) {
236 // makes the match function that will be performed for one property
237 // using propDefn to determine what the match function is.
238 // Function created will return true or false when called.
239
240 // preMatchFns are functions, if any, that are to be performed serially
241 // before the actual match test is performed; eg, replace variables
242 var preMatchFns = [];
243
244 // matchTests is an array of functions;
245 // one for each match test for this property
246 // if any of the matchTests is true, the match test is true (ie; or)
247 // - eg; !prop exists || (doesn't matter)
248 var matchTests = [];
249
250 if(propDefn.variablesAllowed) {
251 preMatchFns.push(makeReplaceVariableFn());
252 }
253
254 if(propDefn.regExpMatch) {
255 matchTests.push(makeRegExpMatchTestFn());
256 } else {
257 matchTests.push(equalTest);
258 }
259
260 if(propDefn.matchIfPObjPropMissing) {
261 matchTests.push(pObjPropMissingTest);
262 }
263
264 if(propDefn.matchIfTObjPropMissing) {
265 matchTests.push(tObjPropMissingTest);
266 }
267
268 var returnFn = preMatchFns.length === 0 ?
269 standardMatchFn : standardMatchWithPreMatchFn;
270
271 return returnFn;
272
273 function standardMatchFn(pObjProp, tObjProp, cb) {
274 async.some(
275 matchTests,
276 function(matchTest, cb) {
277 return matchTest(pObjProp, tObjProp, cb);
278 },
279 function(matches) {
280 return cb(matches);
281 }
282 );
283 }
284
285 function standardMatchWithPreMatchFn(pObjProp, tObjProp, cb) {
286 //create a new array with the iniating function at 0 followed
287 // by the preMatchFns
288 var preMatchExecFns = [
289 function(cb) {
290 return cb(null, pObjProp, tObjProp);
291 }].concat(preMatchFns);
292
293 async.waterfall(
294 preMatchExecFns,
295 function(err, pObjProp, tObjProp) {
296 async.some(
297 matchTests,
298 function(matchTest, cb) {
299 return matchTest(pObjProp, tObjProp, cb);
300 },
301 function(matches) {
302 return cb(matches);
303 }
304 );
305 }
306 );
307 }
308
309
310 function makeRegExpMatchTestFn() {
311 var flags = "";
312 var anchorStart = propDefn.regExpAnchorStart === true ? "^" : "";
313 var anchorEnd = propDefn.regExpAnchorEnd === true ? "$" : "";
314
315 flags = propDefn.regExpIgnoreCase === true ? flags.concat("i") : flags;
316
317 function regExpMatchTestFn(pObjProp, tObjProp, cb) {
318 // turn into RegExp object if it isn't already
319 // NOTE: if RegExp object is passed in, the RegExp is taken as-is
320 // and these options are IGNORED
321 // regExpIgnoreCase
322 // regExpAnchorStart
323 // regExpAnchorEnd
324 var re = _.isRegExp(pObjProp.value) ?
325 pObjProp.value :
326 new RegExp
327 (anchorStart + pObjProp.value + anchorEnd, flags);
328 return cb(pObjProp.exists &&
329 re.test(tObjProp.value));
330 } // end of regExpMatchTestFn
331
332 return regExpMatchTestFn;
333 } // end of makeRegExpMatchTestFn
334
335
336 function equalTest(pObjProp, tObjProp, cb) {
337 return cb(pObjProp.exists &&
338 pObjProp.value === tObjProp.value);
339 }
340
341 function pObjPropMissingTest(pObjProp, tObjProp, cb) {
342 return cb(!pObjProp.exists);
343 }
344
345 function tObjPropMissingTest(pObjProp, tObjProp, cb) {
346 return cb(!tObjProp.exists);
347 }
348
349
350 function makeReplaceVariableFn() {
351 var variablesStartStr = escapeStr(propDefn.variablesStartStr);
352 var variablesEndStr;
353 var variableNameMatchRegExp;
354
355 if(_.isNull(propDefn.variablesEndStr) ) {
356 variableNameMatchRegExp = new RegExp(variablesStartStr + '(\\w*)',"g");
357 } else {
358 variablesEndStr = escapeStr(propDefn.variablesEndStr);
359 variableNameMatchRegExp =
360 new RegExp(
361 variablesStartStr +
362 '(.*)' + // parantheses indicate the variable name being extracted
363 variablesEndStr,"g");
364 }
365
366 return function(pObjProp, tObjProp,cb) {
367 propDefn.getVariables(function(err, variables) {
368 replaceVariable(
369 pObjProp,
370 tObjProp,
371 variables,
372 function(err, pObjProp, tObjProp) {
373 return cb(err, pObjProp, tObjProp);
374 });
375 });
376 };
377
378 function escapeStr(str) {
379 // http://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript
380 return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
381 }
382
383 function replaceVariable(pObjProp, tObjProp, variables, cb) {
384
385 if(pObjProp.exists && _.isString(pObjProp.value)) {
386 // if the pObj property value contains one variable name only,
387 // without any other characters before or after
388 // (except the variablesStartStr & variablesEndStr)
389 // then simply replace the variable name with the variable value
390 // rather than doing a string replace.
391 // This retains the a variable value that is an object (eg Reg Exp)
392 // rather than converting it to a string.
393 var varName =
394 pObjProp.value.replace(new RegExp("^" + variablesStartStr), "");
395 varName = varName.replace(new RegExp(variablesEndStr + "$"), "");
396 if (variables.hasOwnProperty(varName)) {
397 pObjProp.value = variables[varName];
398 } else if (_.isString(pObjProp.value)) {
399 pObjProp.value = pObjProp.value
400 .replace(variableNameMatchRegExp,getVariableValue);
401 }
402 }
403 return cb(null, pObjProp, tObjProp);
404
405 function getVariableValue(match, variableName) {
406 return variables[variableName];
407 }
408
409 } // end of replaceVariable fn
410 } // end of makeReplaceVariableFn
411
412}
413
414function getPropRefs (pObj, tObj, propName, callback) {
415 // returns reference to obj property where propName is in
416 // dot notation as a string
417 // for example obj = {cat: {dog: 1}}
418 // propName = "cat.dog"
419 // returns reference to obj.cat.dog => 1
420 // returns object as
421 // {pObjProp:
422 // {value: (reference to property value),
423 // exists: (true|false if the property exists)},
424 // {tObjProp:
425 // {value: (reference to property value),
426 // exists: (true|false if the property exists)},
427 //
428 var propNameKeys = propName.split(".");
429
430 var propRefsObj = {pObjProp: {}, tObjProp: {}};
431
432 getPropRef(pObj, propNameKeys, function(err, pObjPropRef) {
433 getPropRef(tObj, propNameKeys, function(err, tObjPropRef) {
434 propRefsObj.pObjProp = pObjPropRef;
435 propRefsObj.tObjProp = tObjPropRef;
436 return callback(err, propRefsObj);
437 });
438 });
439
440} //end of getPropRefs
441
442
443function getPropRef (obj, propNameKeys, callback) {
444 // propNameKeys would have already been determined to be a valid
445 // string (converted to Array here) during function creation
446 var propRef = obj;
447 var propExists;
448 var i = 0;
449 var len = propNameKeys.length;
450 var propRefObj = {value: null, exists: null};
451
452 async.whilst(
453 function() { //test performed before fn
454 return _.isObject(propRef) &&
455 propRef.hasOwnProperty(propNameKeys[i]) &&
456 i < len;
457 },
458 function(cb) { //fn performed when test is true
459 propRef = propRef[propNameKeys[i]];
460 i++;
461 return cb(null);
462 },
463 function(err) { //called when test finally fails
464 // property exists if made it through all the prop names
465 propExists = i === len ? true : false;
466 propRefObj.value = propRef;
467 propRefObj.exists = propExists;
468 return callback(null, propRefObj);
469 }
470 );
471
472} // end of getPropRef
473
474function objectPropertyMap(obj, fullPropPath, cb) {
475 //executes cb on each obj property regardless of depth
476 //fullPropPath is optional
477 //cb is called with 4 parameters
478 // 1. error - null if none
479 // 2. Property name as a string (lowest level only) (eg; "city")
480 // 3. Property value (reference to the property value)
481 // 2. Full property path as a string in dot notation (eg; "client.address.city")
482 if(_.isFunction(fullPropPath)) {
483 cb = fullPropPath;
484 fullPropPath = "";
485 }
486
487 var props = Object.keys(obj);
488
489 for (var i = 0; i <= props.length -1; i++) {
490 var newFullPropPath =
491 fullPropPath === "" ?
492 props[i] :
493 fullPropPath + "." + props[i];
494 if(_.isObjectLiteral(obj[props[i]])) {
495 objectPropertyMap(obj[props[i]], newFullPropPath, cb);
496 } else {
497 cb(null, props[i], obj[props[i]], newFullPropPath);
498 }
499 }
500}
501
502function makeFilterPatternObjectsFn(props, options) {
503
504 var propMatchFns = getPropMatchFns(props, options);
505
506 return function(pObjs, tObj,cb) {
507 return filterPatternObjects(pObjs, tObj, propMatchFns, cb);
508 };
509}
510
511function filterPatternObjects(pObjs, tObj, propMatchFns, cb) {
512 if (_.isFunction(propMatchFns)) {
513 cb = propMatchFns;
514 propMatchFns =
515 getPropMatchFns(getObjectProperties(pObjs[0]), {regExpMatch: true});
516 }
517
518 return async.filter(
519 pObjs,
520 function(pObj, cb) {
521 matches(
522 pObj,
523 tObj,
524 propMatchFns,
525 function(doesMatch){
526 return cb(doesMatch);
527 });
528 },
529 function(result) {
530 return cb(result);
531 }
532 );
533}
534
535function makeFilterTargetObjectsFn(props, options) {
536
537 var propMatchFns = getPropMatchFns(props, options);
538
539 return function(pObj, tObjs,cb) {
540 return filterTargetObjects(pObj, tObjs, propMatchFns, cb);
541 };
542}
543
544function filterTargetObjects(pObj, tObjs, propMatchFns, cb) {
545 if (_.isFunction(propMatchFns)) {
546 cb = propMatchFns;
547 propMatchFns =
548 getPropMatchFns(getObjectProperties(pObj), {regExpMatch: true});
549 }
550
551
552 return async.filter(
553 tObjs,
554 function(tObj, cb) {
555 matches(
556 pObj,
557 tObj,
558 propMatchFns,
559 function(doesMatch){
560 return cb(doesMatch);
561 });
562 },
563 function(result) {
564 return cb(result);
565 }
566 );
567}
568
569
570module.exports = {
571 makeMatchFn: makeMatchFn,
572 getPropDefns: getPropDefns,
573 getPropMatchFns: getPropMatchFns,
574 objectPropertyMap: objectPropertyMap,
575 matches: matches,
576 makeFilterTargetObjectsFn: makeFilterTargetObjectsFn,
577 makeFilterPatternObjectsFn: makeFilterPatternObjectsFn,
578 filterPatternObjects: filterPatternObjects,
579 filterTargetObjects: filterTargetObjects,
580 getObjectProperties: getObjectProperties,
581 _: _
582};