diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c5f5696 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..cb3d2ed --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +# /node_modules and /bower_components ignored by default + +# Ignore built files except build/index.js +build/ +target/ +mocha.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..fea6753 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,15 @@ +{ + "rules": { + "no-console": 0 + }, + "parserOptions": { + "ecmaFeatures": { + "modules": true + }, + "sourceType": "module" + }, + "env": { + "node": true, + "es6": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc8adac --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Node Modules +node_modules/* + +# Gradle stuff +target/ +.gradle/ + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db +*.idea + +# Test and Coverage Reports # +############################# +mocha.json +/coverage diff --git a/.istanbul-bamboo.yml b/.istanbul-bamboo.yml new file mode 100644 index 0000000..6b675eb --- /dev/null +++ b/.istanbul-bamboo.yml @@ -0,0 +1,9 @@ +reporting: + instrumentation: + preserve-comments: false + complete-copy: false + save-baseline: false + print: summary + reports: + - clover + diff --git a/.istanbul-local.yml b/.istanbul-local.yml new file mode 100644 index 0000000..ea83202 --- /dev/null +++ b/.istanbul-local.yml @@ -0,0 +1,9 @@ +reporting: + instrumentation: + preserve-comments: false + complete-copy: false + save-baseline: false + print: summary + reports: + - lcov + diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..0644545 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,133 @@ +2.0.0 / 2017-03-01 +======================== + * prep for npm publish + * rename "barry" to "brie" + * package.json updates for public module + +1.0.0 / 2017-02-28 +======================== + * added "type" check as "is" + * Full test suite + * Full test coverage + +0.13.0 / 2017-02-17 +======================== + * added "test" command to scripts + * JiraId: UI-12235 updates lodash dependency from lodash-node to lodash@4.0.0 + * JiraId: UI-12235 updates mocha dependency to 3.2.0-compatible + +0.11.0 / 2014-10-07 +======================== + * JiraId: UI-1434 corrects bug with allowIDs that fails on strict equality; performs type conversion between string ID and numeric ID + +0.10.9 / 2014-09-02 +======================== + * JiraId: UI-1240 corrects 0-check for percentMax values + +0.10.8 / 2014-08-22 +======================== + * JiraId: UI-1206 corrects "check" method exception; + * JiraId: UI-1206 evaluates feature criteriaLogic; + * JiraId: UI-1206 moves feature evaluator setup to barry setup method. + +0.10.7 / 2014-08-22 +======================== + * JiraId: UI-1206 modification in percentScale + +0.10.6 / 2014-08-22 +======================== + * JiraId: UI-1206 date needs an "equality" + +0.10.5 / 2014-08-22 +======================== + * JiraId: UI-1206 number needs an "equality" + +0.10.4 / 2014-08-22 +======================== + * JiraId: UI-1206 method name change; not all data blocks are "user", so not all ID matches are on UserId + +0.10.3 / 2014-08-19 +======================== + * JiraId: UI-1219 creates blank versions of data concern based on pristine source, then extends a clone. + +0.10.2 / 2014-08-19 +======================== + * JiraId: UI-1218 string values not being converted to boolean; introduces booleanify method for overrides + * JiraId: UI-1218 lodash "isEqual" check for empty arrays of criteria response; returns (bool) false by default. + +0.10.1 / 2014-08-19 +======================== + * JiraId: UI-1211 backs out of "extend" paradigm for overrides. + +0.10.0 / 2014-08-19 +======================== + * JiraId: UI-1210 allows code to pass "overrides" property with arguments to allFeatures() + +0.9.3 / 2014-08-19 +======================== + * JiraId: UI-1209 corrects un-handled typecheck in r_engine. cannot call Object.keys on non-object types + + 0.9.2 / 2014-08-18 +======================== + * JiraId: UI-1105 Includes a too-aggressive sting-to-date conversion gate, to capture non-object date values. This leaves some room for improvement. + + 0.9.1 / 2014-08-18 +======================== + * JiraId: UI-1105 Adds date comparisons for older/newer. + +0.8.7 / 2014-08-18 +======================== + * JiraId: UI-1105 reverts string comparison for "above" and "below" to strict >= and <= evaluators. Lexigraphic comparison may cause confusion but is, strictly speaking, the more-expected operation. + +0.8.2-0.8.6 / 2014-08-12 +======================== + * JiraId: UI-1105 "above" and "below" comparison for object attempts to compare original object to extended object + * JiraId: UI-1105 adds new comparison for "object" + * JiraId: UI-1105 adds "above" and "below" for number + * JiraId: UI-1105 adds "above" and "below" for string + +0.8.1 / 2014-08-09 +================== + * JiraId: UI-1105 minor correction in log output + +0.8.0 / 2014-08-09 +================== + * JiraId: UI-1105 adds comparison mechanisms for number and string + * JiraId: UI-1105 converts Barry "has" method to use embedded comparison mechanisms + +0.7.0 / 2014-08-08 +================== + * JiraId: UI-1105 adjusts criteria validation to accept an array instead of singleton. Bolsters engine processing to accurately calculate criteria arrays. + * JiraId: UI-1105 accommodates criteriaLogic of "any" or "all", using the lodash "some" or "every" methods + + +0.6.2 / 2014-08-07 +================== + * JiraId: UI-1105 requiring lodash-node instead of undefined "lodash" + +0.6.1 / 2014-08-06 +================== + * JiraId: UI-1105 adding history.md + * JiraId: UI-1105 updating barry logic and bumping minor version + +0.5.0 / 2014-08-01 +================== + + * JiraId: UI-1133 Added Has method. Stubbed hasMore and hasFewer. Logs modification. + * JiraId: UI-1133 Removed wayward "flags" argument + * JiraId: UI-1133 ...and bumped minor version + +0.4.5 / 2014-07-27 +================== + + * JiraId: UI-1105 resolving local data arguments instead of global; fflip always passes context with method calls. + * JiraId: UI-1105 resolving local data arguments instead of global; fflip always passes context with method calls. + +0.4.4 / 2014-07-26 +================== + * JiraId: UI-1105 diagnostics, logging and clean execution + * JiraId: UI-1105 minor change ==> bump minor version + * JiraId: UI-1105 building out processing and constructor; drops dependency on RSVP + * JiraId: UI-1105 requires fflip, RSVP and lodash + * JiraId: UI-1105 correcting definition in package.json + * JiraId: UI-1046 defines README for barry diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..00e9fbb --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..88e4351 --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +This is brie +============= +This Business Rules Integration Engine (B.R.E, or "brie") is a transient Feature Flipping Criteria System for Node. + +``` +npm install brie +``` + +## Getting Started +Below is a simple example that uses __brie__ to deliver feature flags based on a determined (set) of User Variant(s): +```javascript +// Include brie +var brie = require('brie'); + +// given an inbound data object called "user" plus a set of features (see below) called "featureSet" +brie.setup( + { + data: user, + features: featureSet + } +); +var flags = brie.getAll(); +// expect {feature1: true, feature2: false, ... } from get() or getAll() +``` + +### Criteria +Criteria are the predefined rule types that __brie__ tests data against, per feature. Each feature in the feature set consists of at least one criteria. A feature may contain multiple criteria, joined across a logical "and" or "any" as dictated by the `criteriaLogic`: +```javascript + var featureSet = multiPartTestCase : { + criteria : [ + { + has: { + "trait": "messageCount", + "comparison": "above", + "value": 2 + } + }, + { + has: { + "trait": "creationDate", + "comparison": "older", + "value": "12/Dec/2000" + } + } + ], + criteriaLogic: "any" + }; +// expect {multiPartTestCase: true} from get() and getAll() +``` + + +__brie__ has: + + +#### allowIDs (object, array) +Provided an object containing an "id" property, brie evaluates the presence of the id in the given array, checking for 1 or more entries. + +#### always (object, bool) +`Always` asks brie to "always" respond with the given input. Why? Code consistency, mainly. + +#### percentScale(object, opts) +``` + opts = { + percentMin:[0-1], + percentMax:[0-1], + salt:[number], + testPhase:[string] + } +``` +"testPhase" is used for logging and debugging only, and does not impact the algorithm. + +The `object.id` is used to calculate a percentage, modified by the salt value. If the resulting, salted, number is within range, returns true; otherwise false. + +#### has(test data [object], comparison data [object]) +The most complex criteria mechanism, `has` will evaluate the test data (first argument) against the trait, comparator and value provided in the second argument. If the test data has the trait and the associated value evaluates properly considering the comparison value, then true is returned. + +* Requires a data object +* Requires a set of comparison data containing the following properties: + * trait (required) - the name of a property to find on data object. If comparison and value are omitted, the evaluation will simply verify the existence of the property on the object. + * comparison (optional) - one of an existing set of comparison instructions : + * equals (string, date, number, object) + * like (string) + * below (string, date, number, object) + * above (string, date, number, object) + * longer (string, date, number, object) + * shorter (string, date, number, object) + * older (date) + * younger (date) + * value (optional) - static value for comparison + +If the comparison object contains only `trait` then brie will evaluate the presence of the property on the data object. Comparison and value must be provided together, when one of them is provided. + +**Note:** for the purpose of comparisons, "object" refers to both `object` and `array`, with the distinction being left up to the return value of the [_.isArray() method](https://lodash.com/docs#isArray). + +Possible comparisons are: + +##### equals + * (`date`) - a **strict** equality check between base comparison date and check date. If the check value is a number, then brie evaluates the difference between `now()` and the comparison date to be equal to the check value, in days (e.g. "0" implies "today"). + * (`numeric`) - a **non-strict** equality check. + * (`object`) - a **deep comparison** equality check, using the [_.isEqual()](https://lodash.com/docs#isEqual) method from `lodash-node`. + * (`string`) - a **strict** equality check that the comparison string is identical to the check value note that this is a case-sensitive check. See "like", below. + +##### above + * (`date`) - [alias: "longer", "older"] checks to see that the comparison date is older than the check date. If the check value is a number, brie checks to see that the comparison date is at least as old, in days, as the check value, compared to `now()` (e.g. "1" implies the date must be yesterday or older). + * (`numeric`) - [alias: "longer"] a greater-than-or-equal-to check that the comparison number is greater than the check value. + * (`object`) - an array or object is said to be "above" another if it fully contains the other. That is: if the data object contains the comparison object, then the data object is above the comparison object. If the objects are equal, the `above` comparison is inherently false. If the data object is a non-array object but the comparison object is an array, then the [_.difference()](https://lodash.com/docs#difference) comparison is done between the keys of the data object and the comparison array. If both data and comparison objects are arrays, a lodash [_.difference()](https://lodash.com/docs#difference) between comparison and data is compare with `[]`, indicating that the data object fully contains the comparison object. + * (`string`) - performs a javascript string "greater-than-or-equal" comparison. As a loosely-typed language, this could be anything, really. + +##### below + * (`date`) - [alias: "shorter", "younger"] checks to see that the comparison date is more recent than the check date. If the check value is a number, brie checks to see that the comparison date is at least as new, in days, as the check value, compared to `now()` (e.g. "1" implies the date must be tomorrow or younger). + * (`numeric`) - [alias: "shorter"] a less-than-or-equal-to check that the comparison number is smaller than the check value. + * (`object`) - an array or object is said to be "below" another if it is fully contained in the other. That is: if the comparison object contains the data object, then the data object is below the comparison object. If the objects are equal, the `below` comparison is inherently false. If the data object is a non-array object but the comparison object is an array, then the [_.difference()](https://lodash.com/docs#difference) comparison is done between the keys of the data object and the comparison array. If both data and comparison objects are arrays, a lodash [_.difference()](https://lodash.com/docs#difference) between comparison and data is compare with `[]`, indicating that the data object fully contains the comparison object. + * (`string`) - performs a javascript string "less-than-or-equal" comparison. As a loosely-typed language, this could be anything, really. + +##### like + * (`string`) - a **non-strict** check that the `toLowerCase()` comparison string is equal to the `toString().toLowerCase()` check value. + +##### longer + * (`object`) - true if the [_.size()](https://lodash.com/docs#size) of the data object is greater than that of the comparison object. + * (`string`) - true if the length of the comparison string (non-trimmed) is greater than or equal to the length of the check value (non-trimmed). + +##### shorter + * (`object`) - true if the [_.size()](https://lodash.com/docs#size) of the data object is less than that of the comparison object. + * (`string`) - true if the length of the comparison string (non-trimmed) is less than or equal to the length of the check value (non-trimmed). + +#### is (object (source), object (options)) +Tests if a key from a source object has property of a noted __type__. +##### Options + * (`trait`) - JSON notation path reference for a key, presumed to exist in source data. Checks source data using lodash [_.get()](https://lodash.com/docs/#get), and follows dot-path rules. + * (`type`) - string representing potential data type. String case is adjusted in the `is` method, but value must be one of: + * array + * boolean + * date (**NOTE:** a value like `Tue Feb 28 2017 20:42:48 GMT-0800 (PST)` is a _string_ type and not _date_.) + * empty + * finite + * function + * integer + * NaN + * nil + * null + * number + * object + * regex \| regular_expression \| regexp + * string + * undefined +### Features +Features contain sets of criteria to test users against. The value associated with the criteria is passed in as the data argument of the criteria function. A user will have a featured enabled if they match all listed criteria, otherwise the feature is disabled. Features can include other optional properties for context. Features are described as follows: +```javascript +var ExampleFeaturesObject = { + "canCheckAlways": { + "criteria": [ + { + "always": false + } + ] + }, + "canCheckHasString": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ] + }, + "canCheckType": { + "criteria": [ + { + "is": { + "trait": "myData.nested.key", + "type": "number" + } + } + ] + }, + "canCheckHigherNumber": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "above", + "value": 1 + } + } + ] + }, + "canCheckLowerDate": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "younger", + "value": new Date(2000, 1, 1, 1, 22, 0) + } + } + ] + } +} +``` + +## Usage +``` +Object setup(options) // brie needs to be initialized with a data object and a set of features. Don't try to make brie do stuff without setting up first. That makes brie angry. +Bool get(string feature) // asks brie if the feature is enabled. +Object getAll() // requests the full evaluation of features from brie +``` +### Setup +In the setup (initializer), brie accepts: +* (`object`) - data: a object structure containing the data to be tested. +* (`object`) - features: see feature section, above. +* (`object`) - overrides: containing properties that match feature names, an override is a simple set of boolean boolean values to force a feature into a state. + + ``` + { + "canCheckAlways": true + } + ``` +* (`bool`) - showLogs: when true, brie is verbose about the request activities. + +Returns `brie`. + +Once initialized, via `setup()`, brie can be queried for the outcome of any or all features. + +### get +`get` returns the outcome of a single feature, and requires the string name of the feature, as an argument, and is invoked as +```javascript +var isHigherNumber = brie.get('canCheckHigherNumber'); +// isHigherNumber = true; +``` + +### getAll +`getAll` returns a hash of all features and their evaluated value, including overrides, as a shallow javascript object. +```javascript +var allFeatures = brie.getAll(); +// allFeatures = { +// canCheckAlways: true, +// canCheckHasString: true, +// canCheckHigherNumber: false, +// canCheckLowerDate: false +// } +``` + +### Chaining +A single method can be chained against the `setup` method. `setup` returns `brie`, which has both a `get` and `getAll` method. Further chaining is not available, since `get` returns a boolean and `getAll` returns an object. +```javascript +var allFeatures = brie.setup({ + data: data_in, + features: flags_in, + overrides: overrides, + showLogs: true +}).getAll() +``` diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/lib/brie.js b/lib/brie.js new file mode 100644 index 0000000..1ce8afe --- /dev/null +++ b/lib/brie.js @@ -0,0 +1,97 @@ +'use strict'; + + +/** + * This is brie + * ============= + * This Business Rules Engine (B.R.E, or "brie") is a transient Feature Flipping Criteria System for Node. + * + */ +var flr = require('./ext/rules_engine'), + some = require('lodash/some'), + every = require('lodash/every'), + cloneDeep = require('lodash/cloneDeep'), + extend = require('lodash/assignIn'), + concat = require('lodash/concat'), + isArray = require('lodash/isArray'), + isDate = require('lodash/isDate'), + size = require('lodash/size'), + debug = require('debug')('brie:core'), + + brie = module.exports = (function () { + var features = {}, + data = {}, + overrides = {}, + functionalCriteria = {}, + getAllFlags = function () { + return flr.allFeatures(data, overrides); + }, + check = function (flagName) { + if (features.hasOwnProperty(flagName)) { + var featureMatch = flr.hasFeature(data, flagName); + return (features[flagName].hasOwnProperty('criteriaLogic') && features[flagName]['criteriaLogic'] === 'any') + ? some(featureMatch) + : every(featureMatch); + } + return false; + }, + setup = function (opts) { + var optCriteria = opts.criteria || {}, + optFeatures = opts.features || {}, + optOverrides = opts.overrides || {}, + optContext = opts.data || {}; + functionalCriteria = cloneDeep(this.criteria, true); + extend(functionalCriteria, optCriteria); + features = optFeatures; + overrides = optOverrides; + extend(data, optContext); + flr.setup({ + criteria: functionalCriteria, + features: features + }); + return brie; + }, + factory = { + getDiagnostics: function () { + return { + 'criteria': this.criteria, + 'features': features, + 'data': data, + 'getType': factory.getType + }; + }, + getType: function (ev) { + if (typeof ev === 'object') { + return (isArray(ev)) ? "array" : (isDate(ev)) ? "date" : "object"; + } + return (typeof ev); + } + }; + return { + setup: setup, + getAll: getAllFlags, + get: check, + diagnostics: factory.getDiagnostics + } + })(); + + +brie.knows = function (f) { + f.apply(brie); + return brie; +}; +//brie.comp = {}; +brie.criteria = {}; +brie.determine = {}; +brie + .knows(require('./criteria/always')) + .knows(require('./criteria/allowIds')) + .knows(require('./criteria/has')) + .knows(require('./reflection/is')) + .knows(require('./criteria/percentScale')); +brie + .knows(require('./comparators/number')) + .knows(require('./comparators/object')) + .knows(require('./comparators/string')) + .knows(require('./comparators/date')) + .knows(require('./reflection/is')); diff --git a/lib/comparators/date.js b/lib/comparators/date.js new file mode 100644 index 0000000..266e664 --- /dev/null +++ b/lib/comparators/date.js @@ -0,0 +1,45 @@ +var debug = require('debug')('brie:knows:date'), + isDate = require('lodash/isDate'), + assign = require('lodash/assign'); + +module.exports = function () { + var brie = this, + date = { + equals: function (baseVal, checkVal) { + debug('Comparing "' + baseVal + '" === "' + checkVal + '": ' + (baseVal <= checkVal)); + if (isDate(checkVal)) { + return baseVal === checkVal; + } + if (!isNaN(checkVal)) { + debug('Comparing date "' + baseVal + '" against number "' + checkVal + '". Assuming "now()" as current date and "day" as the numeric differentiation.'); + return (((new Date()).getTime() - (new Date(baseVal)).getTime() / 86400000) === checkVal); + } + return false; + }, + older: function (baseVal, checkVal) { + debug('Comparing "' + baseVal + '" <= "' + checkVal + '": ' + (baseVal <= checkVal)); + if (isDate(checkVal)) { + return baseVal <= checkVal; + } + if (!isNaN(checkVal)) { + debug('Comparing date "' + baseVal + '" against number "' + checkVal + '". Assuming "now()" as current date and "day" as the numeric differentiation.'); + return (((new Date()).getTime() - (new Date(baseVal)).getTime()) / 86400000 >= checkVal); + } + return false; + }, + younger: function (baseVal, checkVal) { + debug('Comparing "' + baseVal + '" >= "' + checkVal + '": ' + (baseVal >= checkVal)); + if (isDate(checkVal)) { + return baseVal >= checkVal; + } + if (!isNaN(checkVal)) { + debug('Comparing date "' + baseVal + '" against number "' + checkVal + '". Assuming "now()" as current date and "day" as the numeric differentiation.'); + return ((((new Date()).getTime() - (new Date(baseVal)).getTime()) / 86400000) <= checkVal); + } + return false; + } + }; + assign(brie.determine, { date: date }); + brie.determine.date.longer = brie.determine.date.above = brie.determine.date.older; + brie.determine.date.shorter = brie.determine.date.below = brie.determine.date.younger; +}; diff --git a/lib/comparators/number.js b/lib/comparators/number.js new file mode 100644 index 0000000..f883c2c --- /dev/null +++ b/lib/comparators/number.js @@ -0,0 +1,23 @@ +var debug = require('debug')('brie:knows:number'), + assign = require('lodash/assign'); +module.exports = function () { + var brie = this, + numberHandler = { + equals: function (baseVal, checkVal) { + checkVal = checkVal || 0; + debug('Comparing "' + baseVal + '" === "' + checkVal + '": ' + (baseVal === parseInt(checkVal, 10))); + return baseVal == parseInt(checkVal, 10); + }, + below: function (baseVal, checkVal) { + debug('Comparing "' + baseVal + '" <= "' + checkVal + '": ' + (baseVal <= parseInt(checkVal, 10))); + return baseVal <= parseInt(checkVal, 10); + }, + above: function (baseVal, checkVal) { + debug('Comparing "' + baseVal + '" >= "' + checkVal + '": ' + (baseVal >= parseInt(checkVal, 10))); + return baseVal >= parseInt(checkVal, 10); + } + }; + assign(brie.determine, { "number": numberHandler }); + brie.determine.number.longer = brie.determine.number.above; + brie.determine.number.shorter = brie.determine.number.below; +}; diff --git a/lib/comparators/object.js b/lib/comparators/object.js new file mode 100644 index 0000000..1834931 --- /dev/null +++ b/lib/comparators/object.js @@ -0,0 +1,66 @@ +var debug = require('debug')('brie:knows:object'), + isEqual = require('lodash/isEqual'), + cloneDeep = require('lodash/cloneDeep'), + isArray = require('lodash/isArray'), + difference = require('lodash/difference'), + keys = require('lodash/keys'), + assignIn = require('lodash/assignIn'), + size = require('lodash/size'); +module.exports = function () { + var brie = this, + object_comparator = { + equals: function (baseObj, checkObj) { + debug('Comparing "' + baseObj + '" with "' + checkObj + '": ' + isEqual(baseObj, checkObj)); + return isEqual(baseObj, checkObj); + }, + above: function (baseObj, checkObj) { + var b = cloneDeep(baseObj), + c = cloneDeep(checkObj); + debug('Comparing "' + baseObj + '" "above" "' + checkObj + '": ' + isEqual(baseObj, checkObj)); + if (brie.determine.object.equals(b, c)) { + debug('Exiting "above" comparison, as both base and comparison objects are (deep) equal'); + return false; + } + if (isArray(b)) { + if (isArray(c)) { + debug('Comparing difference of "' + checkObj + '" (array) and "' + baseObj + '" (array) with the empty array'); + return (isEqual(difference(c, b), [])); // compares simple difference between arrays with the empty array, essentially identifying that b contains c + } + return (isEqual(difference(keys(c), b), [])); // comparing, instead, the b array with the array of Keys from c + } else { + if (isArray(c)) { + return (isEqual(difference(c, keys(b), []))); + } + return isEqual(assignIn(c, b), b); + } + }, + below: function (baseObj, checkObj) { + var b = cloneDeep(baseObj), + c = cloneDeep(checkObj); + debug('Comparing "' + baseObj + '" "below" "' + checkObj + '": ' + isEqual(baseObj, checkObj)); + if (brie.determine.object.equals(b, c)) { + debug('Exiting "below" comparison, as both base and comparison objects are (deep) equal'); + return false; + } + if (isArray(b)) { + if (isArray(c)) { + debug('Comparing difference of "' + baseObj + '" (array) and "' + checkObj + '" (array) with the empty array'); + return (isEqual(difference(b, c), [])); // compares simple difference between arrays with the empty array, essentially identifying that b contains c + } + return (isEqual(difference(keys(b), c), [])); // comparing, instead, the b array with the array of Keys from c + } else { + if (isArray(c)) { + return (isEqual(difference(b, keys(c), []))); + } + return isEqual(assignIn(b, c), c); + } + }, + shorter: function (baseObj, checkObj) { + return size(baseObj) <= size(checkObj); + }, + longer: function (baseObj, checkObj) { + return size(baseObj) >= size(checkObj); + } + }; + assignIn(brie.determine, { "object": object_comparator, "array": object_comparator }); +}; diff --git a/lib/comparators/string.js b/lib/comparators/string.js new file mode 100644 index 0000000..b6ca140 --- /dev/null +++ b/lib/comparators/string.js @@ -0,0 +1,37 @@ +var debug = require('debug')('brie:knows:string'), + isNil = require('lodash/isNil'), + assignIn = require('lodash/assignIn'); +module.exports = function () { + var brie = this, + stringHandler = { + equals: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + return baseVal === checkVal.toString(); + }, + like: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + return baseVal.toLowerCase() == checkVal.toString().toLowerCase(); + }, + below: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + debug('Performing string comparison using ">" or "<". This may not be the comparison you are looking for.'); + debug('Comparing "' + baseVal + '" <= "' + checkVal.toString() + '": ' + (baseVal <= checkVal.toString())); + return (baseVal <= checkVal.toString()); + }, + above: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + debug('Performing string comparison using ">" or "<" is probably not be the comparison you are looking for.'); + debug('Comparing "' + baseVal + '" >= "' + checkVal.toString() + '": ' + (baseVal >= checkVal.toString())); + return (baseVal >= checkVal.toString()); + }, + longer: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + return baseVal.length >= checkVal.toString().length; + }, + shorter: function (baseVal, checkVal) { + if (isNil(checkVal)) checkVal = ""; + return baseVal.length <= checkVal.toString().length; + } + }; + assignIn(brie.determine, { "string": stringHandler }); +}; diff --git a/lib/criteria/allowIds.js b/lib/criteria/allowIds.js new file mode 100644 index 0000000..1755d53 --- /dev/null +++ b/lib/criteria/allowIds.js @@ -0,0 +1,16 @@ +var filter = require('lodash/filter'), + size = require('lodash/size'), + isArray = require('lodash/isArray'), + assignIn = require('lodash/assignIn'), + debug = require('debug')('brie:criteria:allowIds'); +module.exports = function () { + var brie = this, + allow_ids = function (data_in, idArr) { + debug('Validating inclusion of id in "' + idArr + '"'); + var idMatch = filter(data_in, function (d) { + return parseInt(d, 10) === parseInt(data_in['id'], 10); + }); + return (isArray(idArr) && data_in.hasOwnProperty('id') && size(idMatch) > 0); + }; + assignIn(brie.criteria, { allowIDs: allow_ids }); +}; diff --git a/lib/criteria/always.js b/lib/criteria/always.js new file mode 100644 index 0000000..d32c49d --- /dev/null +++ b/lib/criteria/always.js @@ -0,0 +1,10 @@ +var assignIn = require('lodash/assignIn'), + debug = require('debug')('brie:criteria:always'); +module.exports = function () { + var brie = this, + always = function (data_in, val) { + debug('Simple execution for "' + JSON.stringify(data_in) + '" always: "' + val.toString() + '"'); + return val; + }; + assignIn(brie.criteria, { always: always }); +}; diff --git a/lib/criteria/has.js b/lib/criteria/has.js new file mode 100644 index 0000000..6add74b --- /dev/null +++ b/lib/criteria/has.js @@ -0,0 +1,45 @@ +var assignIn = require('lodash/assignIn'), + debug = require('debug')('brie:criteria:has'); +module.exports = function () { + var brie = this, + getType = this.diagnostics().getType, + has = function (data_in, c_data) { + var traitVal, + hasProp = false; + debug('======================================== :: brie :: has'); + debug("\"has\" argument(s): ", c_data); + if (c_data.hasOwnProperty('trait') && data_in.hasOwnProperty(c_data['trait'])) { + traitVal = data_in[c_data['trait']]; + hasProp = true; + } + debug("Evaluation data has trait (" + c_data['trait'] + ")? ", hasProp); + if (hasProp) { + debug("Trait value (if found): ", traitVal); + debug("Trait type: ", typeof traitVal); + if (c_data.hasOwnProperty('comparison') && c_data.hasOwnProperty('value')) { + var type = getType(data_in[c_data['trait']]).toLowerCase(); + try { + var retOption = brie.determine[type][c_data['comparison']].apply(brie, [data_in[c_data['trait']], c_data['value']]); + debug("Comparison outcome of brie.determine." + type + "." + c_data['comparison'] + "(" + [data_in[c_data['trait']], c_data['value']] + "):", retOption); + debug('======================================== :: end brie :: has'); + return retOption; + } catch (e) { + debug('********** Exception handled in "has" method. **********'); + debug('Unrecognized type or comparator:\n ', e.message); + debug("Data Trait: ", c_data['trait']); + debug("Trait Value: ", data_in[c_data['trait']]); + debug("type: ", type); + debug("comparator: ", c_data['comparison']); + debug("Sorry. brie does not have a method called brie.determine." + type + "." + c_data['comparison'] + "()"); + debug("Returning boolean::false, by default."); + debug('********** Exception handled in "has" method. **********'); + debug('======================================== :: end brie :: has'); + return false; + } + } + } + debug('======================================== :: end brie :: has'); + return hasProp; + }; + assignIn(brie.criteria, { has: has }); +}; diff --git a/lib/criteria/percentScale.js b/lib/criteria/percentScale.js new file mode 100644 index 0000000..8f8373f --- /dev/null +++ b/lib/criteria/percentScale.js @@ -0,0 +1,27 @@ +var assignIn = require('lodash/assignIn'), + debug = require('debug')('brie:criteria:percentScale'); +module.exports = function () { + var brie = this, + p_scale = function (data_in, c_data) { + var percentMin = (typeof c_data['percentMin'] === 'number') ? c_data['percentMin'] : 0, + percentMax = (typeof c_data['percentMax'] === 'number') ? c_data['percentMax'] : 100, + salt = c_data['salt'] || 1, + testPhase = c_data['testPhase'] || 'unknown test'; + percentMin = (percentMin < 1) ? percentMin * 100 : percentMin; + percentMax = (percentMax < 1) ? percentMax * 100 : percentMax; + debug('======================================== :: brie :: percentScale'); + debug("(data_in.id*salt % 100)", (data_in.id * salt % 100)); + debug("percentMin", percentMin); + debug("percentMax", percentMax); + debug("salt", salt); + debug("data_in.id", data_in.id); + debug('testPhase', testPhase); + debug("Math.min((data_in.id*salt)%100, percentMin)", Math.min((data_in.id * salt) % 100, percentMin)); + debug("Math.max((data_in.id*salt), percentMax)%100", Math.max((data_in.id * salt) % 100, percentMax)); + var t = Math.min((data_in.id * salt) % 100, percentMin) === percentMin && Math.max((data_in.id * salt) % 100, percentMax) === percentMax; + debug("t", t); + debug('======================================== :: end brie :: percentScale'); + return t; + }; + assignIn(brie.criteria, { percentScale: p_scale }); +}; diff --git a/lib/ext/rules_engine.js b/lib/ext/rules_engine.js new file mode 100644 index 0000000..b8fa44b --- /dev/null +++ b/lib/ext/rules_engine.js @@ -0,0 +1,56 @@ +'use strict'; +var forEach = require('lodash/forEach'), + every = require('lodash/every'), + some = require('lodash/some'), + isEqual = require('lodash/isEqual'), + isNil = require('lodash/isNil'); +function makeBoolean(sCheck) { + return { "true": 1, "1": 1, "yes": 1, "on": 1 }.hasOwnProperty(sCheck.toString().toLowerCase()); +} +var self = module.exports = { + _features: {}, + _criteria: {}, + setup: function (params) { + self._criteria = params.criteria; + self._features = params.features; + }, + hasFeature: function (context, featureName) { + var feature = self._features[featureName]; + if (typeof feature != 'object') { + return null; + } + var featureCriteria = feature.criteria || []; + var isEnabled = []; + forEach(featureCriteria, function (value) { + var criteriaArray = (typeof value === 'object') ? Object.keys(value) : []; + if (criteriaArray.length == 0) { + return [false]; + } + var criteriaSuccess = []; + criteriaArray.forEach(function (cKey) { + var c_data = value[cKey]; + var c_func = self._criteria[cKey]; + criteriaSuccess.push(c_func(context, c_data)); + }); + isEnabled.push(every(criteriaSuccess)); + }); + return (isEqual(isEnabled, [])) ? [false] : isEnabled; + }, + allFeatures: function (context, overrides) { + var featureReconcile = {}; + forEach(self._features, function (value, key) { + var enabled = isNil(value.enabled) ? true : value.enabled; + if (enabled) { + if (isNil(overrides[key])) { + var featureMatch = self.hasFeature(context, key); + featureReconcile[key] = (value.hasOwnProperty('criteriaLogic') && value['criteriaLogic'] === 'any') + ? some(featureMatch) + : every(featureMatch); + } else { + featureReconcile[key] = makeBoolean(overrides[key]); + } + } + }); + return featureReconcile; + } +}; diff --git a/lib/reflection/is.js b/lib/reflection/is.js new file mode 100644 index 0000000..24b1d76 --- /dev/null +++ b/lib/reflection/is.js @@ -0,0 +1,53 @@ +var debug = require('debug')('brie:knows:is'), + _ = require('lodash'), + isNil = require('lodash/isNil'), + isEmpty = require('lodash/isEmpty'), + assignIn = require('lodash/assignIn'); +module.exports = function () { + var brie = this, + handler = function (baseVal, isVal) { + if (isEmpty(isVal)) { // will short-circuit earlier if "is" is null/undefined + debug('No comparison provided.'); + return false; + } + if(isNil(isVal.type)) { + debug('No type provided for comparison.'); + return false; + } + if(isNil(isVal.trait) || !(_.get(baseVal, isVal.trait))) { + debug('No trait available for comparison.'); + return false; + } + + var typeToCheck = isVal.type, + _isHash = { + is_array: _.isArray, + is_boolean: _.isBoolean, + is_date: _.isDate, + is_empty: _.isEmpty, + is_finite: _.isFinite, + is_function: _.isFunction, + is_integer: _.isInteger, + is_nan: _.isNaN, + is_nil: _.isNil, + is_null: _.isNull, + is_number: _.isNumber, + is_object: _.isObject, + is_regex: _.isRegExp, + is_regular_expression: _.isRegExp, + is_regexp: _.isRegExp, + is_string: _.isString, + is_undefined: _.isUndefined + }; + var traitToCheck = _.get(baseVal, isVal.trait); + typeToCheck = typeToCheck.toString().toLowerCase(); + debug('Checking if ' + JSON.stringify(traitToCheck) + ' is of type "' + typeToCheck + '"'); + if (_isHash.hasOwnProperty('is_' + typeToCheck)) { + debug('checking ' + traitToCheck + ' against "is_' + typeToCheck + '"'); + return _isHash['is_' + typeToCheck].call(this, traitToCheck); + } + debug('No known type check for "' + typeToCheck + '". Possible checks are (case-sensitive)'); + return false; + }; + assignIn(brie.criteria, { "is": handler }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..bd39524 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "brie", + "description": "Business Rules Integration Engine (brie). Node module for managing business rules, feature flags, and decision logic", + "author": { + "name": "Jason Corns", + "email": "jason.corns@peopleconnect.us" + }, + "licence": "Internal; Closed.", + "version": "2.0.2", + "private": true, + "main": "lib/brie", + "repository": { + "type": "git", + "url": "https://github.com/peopleconnectus/brie" + }, + "keywords": [ + "feature", + "flip", + "toggle", + "feature flipping", + "feature toggling", + "continuous integration", + "business rules engine", + "business rules management system", + "B.R.E.", + "B.R.M.S", + "BRE", + "BRMS" + ], + "dependencies": { + "lodash": "4.15.0", + "debug": "2.6.0" + }, + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/peopleconnectus/brie/issues" + }, + "devDependencies": { + "istanbul": "^0.4.5", + "mocha": "^3.2.0", + "mocha-bamboo-reporter": "^1.1.1", + "mocha-eslint": "^3.0.1" + }, + "scripts": { + "test": "node node_modules/.bin/mocha", + "cover": "node node_modules/.bin/istanbul cover _mocha -- -R mocha-bamboo-reporter" + } +} diff --git a/test/eslinter.js b/test/eslinter.js new file mode 100644 index 0000000..cb46a3d --- /dev/null +++ b/test/eslinter.js @@ -0,0 +1,15 @@ +var linter = require('mocha-eslint'), + path = require('path'), + rootDir = path.join(__dirname, '../'), + patterns = [ + path.join(rootDir, 'lib'), + path.join(rootDir, 'ext'), + path.join(__dirname, 'eslinter.js') + ], + options = { + alwaysWarn: false + }; + +describe('Runs eslint against codebase', function () { + linter(patterns, options); +}); diff --git a/test/evaluators/complex.js b/test/evaluators/complex.js new file mode 100644 index 0000000..4fecbe6 --- /dev/null +++ b/test/evaluators/complex.js @@ -0,0 +1,255 @@ +/** + * Created by j.corns on 2/22/17. + */ + +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + describe('#complex evaluation', function () { + before(function () { + this.checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true + }; + this.features = { + // combination comparisons + // implied "all" + "canCheckComplexAll": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "below", + "value": 5 + } + }, + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ] + }, + // explicit "any" + "canCheckComplexAny": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "below", + "value": 9999999 + } + }, + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ], + "criteriaLogic": "any" + }, + "canCheckSimpleAny": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "below", + "value": 9999999 + } + }, + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ], + "criteriaLogic": "any" + }, + // "for-ids" check + "canCheckAllowIds": { + "criteria": [ + { + "allowIDs": [1234, 5678, 91011, 123456789] + } + ] + }, + // for "percentScale" check + "canCheckPercentScale": { + "criteria": [ + { + "percentScale": { + percentMin: 0, + percentMax: .4, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleNoMin": { + "criteria": [ + { + "percentScale": { + percentMax: .4, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleNoMax": { + "criteria": [ + { + "percentScale": { + percentMin: .4, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleBadMin": { + "criteria": [ + { + "percentScale": { + percentMin: "zero", + percentMax: .4, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleBadMax": { + "criteria": [ + { + "percentScale": { + percentMin: .1, + percentMax: "point-four", + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleBigMin": { + "criteria": [ + { + "percentScale": { + percentMin: 22, + percentMax: 72, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleBigMax": { + "criteria": [ + { + "percentScale": { + percentMin: 1, + percentMax: 50, + salt: 9, + testPhase: "Can Check Percent Scale" + } + } + ] + }, + "canCheckPercentScaleNoSalt": { + "criteria": [ + { + "percentScale": { + percentMin: 1, + percentMax: 50, + testPhase: "Needs Salt" + } + } + ] + }, + "canCheckPercentScaleNoLabel": { + "criteria": [ + { + "percentScale": { + percentMin: 1, + percentMax: 50, + salt: 9 + } + } + ] + }, + "fullCheckWithOverrides": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: this.checkData, + features: this.features, + overrides: {"fullCheckWithOverrides" : false}, + showLogs: false + }); + + }); + + it('"canCheckComplexAll" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckComplexAll")); + }); + it('"canCheckComplexAny" should evaluate to true', function () { + assert(this.bSetup.getAll()); + }); + it('"canCheckSimpleAny" should evaluate to true', function () { + assert(this.bSetup.get('canCheckSimpleAny')); + }); + it('"fullCheckWithOverrides" should evaluate to false', function () { + assert(!this.bSetup.getAll()['fullCheckWithOverrides']); + }); + it('"canCheckAllowIds" should evaluate to true', function () { + assert(this.bSetup.get("canCheckAllowIds")); + }); + it('percentScale should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScale") === 'boolean'); + }); + it('percentScale with no "minimum" should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleNoMin") === 'boolean'); + }); + it('percentScale with no "maximum" should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleNoMax") === 'boolean'); + }); + it('percentScale with bad "minimum" should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleBadMin") === 'boolean'); + }); + it('percentScale with bad "maximum" should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleBadMax") === 'boolean'); + }); + it('percentScale with minimum over 1 should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleBigMin") === 'boolean'); + }); + it('percentScale with maximum over 1 should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleBigMax") === 'boolean'); + }); + it('percentScale without salt should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleNoSalt") === 'boolean'); + }); + it('percentScale without label should evaluate', function () { + assert(typeof this.bSetup.get("canCheckPercentScaleNoLabel") === 'boolean'); + }); + }); +}; diff --git a/test/evaluators/dates.js b/test/evaluators/dates.js new file mode 100644 index 0000000..108ee7c --- /dev/null +++ b/test/evaluators/dates.js @@ -0,0 +1,158 @@ +/** + * Created by j.corns on 2/22/17. + */ +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + + describe('#date evaluation', function () { + before(function () { + this.staticDate = new Date(); + this.pastDate = new Date(this.staticDate-604800000); + this.checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: this.staticDate, + hasOldDate: this.pastDate, + hasBooleanValue: true + }; + this.features = { + // date comparator + "canCheckEqualDate": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "equals", + "value": this.staticDate + } + } + ] + }, + "canCheckEqualDateNumber": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "equals", + "value": 949396920000 + } + } + ] + }, + "canCheckEqualDateString": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "equals", + "value": "any non-numeric string" + } + } + ] + }, + "canCheckHigherDate": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "older", + "value": new Date() + } + } + ] + }, + "canCheckHigherDateNumber": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "older", + "value": 0.000000011574074074074073 // decimal representation of 1 millisecond. + } + } + ] + }, + "canCheckHigherDateString": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "older", + "value": "any non-numeric string" + } + } + ] + }, + "canCheckLowerDate": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "younger", + "value": new Date(2000, 1, 1, 1, 22, 0) + } + } + ] + }, + "canCheckLowerDateNumber": { + "criteria": [ + { + "has": { + "trait": "hasOldDate", + "comparison": "younger", + "value": 10 + } + } + ] + }, + "canCheckLowerDateString": { + "criteria": [ + { + "has": { + "trait": "hasDateValue", + "comparison": "younger", + "value": "any non-numerica string" + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: this.checkData, + features: this.features, + overrides: {}, + showLogs: false + }); + }); + it('Date equality comparison', function () { + assert(this.bSetup.get("canCheckEqualDate")); + }); + it('Date equality comparison against a number', function () { + assert(!this.bSetup.get("canCheckEqualDateNumber")); + }); + it('Date equality comparison against string', function () { + assert(!this.bSetup.get("canCheckEqualDateString")); + }); + it('Date difference comparison (older)', function () { + assert(this.bSetup.get("canCheckHigherDate")); + }); + it('Date difference comparison against a number (older)', function () { + assert(this.bSetup.get("canCheckHigherDateNumber")); + }); + it('Date equality comparison against string (older)', function () { + assert(!this.bSetup.get("canCheckHigherDateString")); + }); + it('Date difference comparison (younger)', function () { + assert(this.bSetup.get("canCheckLowerDate")); + }); + it('Date difference comparison against a number (younger)', function () { + assert(this.bSetup.get("canCheckLowerDateNumber")); + }); + it('Date difference comparison against string (younger)', function () { + assert(!this.bSetup.get("canCheckLowerDateString")); + }); + }); +}; diff --git a/test/evaluators/numbers.js b/test/evaluators/numbers.js new file mode 100644 index 0000000..3f4a7e2 --- /dev/null +++ b/test/evaluators/numbers.js @@ -0,0 +1,85 @@ +/** + * Created by j.corns on 2/22/17. + */ +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + + describe('#number evaluation', function () { + before(function () { + this.checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true + }; + this.features = { + // number comparators + "canCheckHigherNumber": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "above", + "value": 1 + } + } + ] + }, + "canCheckLowerNumber": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "below", + "value": 9999999 + } + } + ] + }, + "canCheckEqualNumber": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "equals", + "value": 181818 + } + } + ] + }, + "canCheckInvalidNumber": { + "criteria": [ + { + "has": { + "trait": "hasNumberValue", + "comparison": "equals", + "value": null + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: this.checkData, + features: this.features, + overrides: {}, + showLogs: false + }); + }); + it('"canCheckHigherNumber" should evaluate to true', function () { + assert(this.bSetup.get("canCheckHigherNumber")); + }); + it('"canCheckLowerNumber" should evaluate to true', function () { + assert(this.bSetup.get("canCheckLowerNumber")); + }); + it('"canCheckEqualNumber" should evaluate to true', function () { + assert(this.bSetup.get("canCheckEqualNumber")); + }); + it('"canCheckInvalidNumber" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckInvalidNumber")); + }); + }); +}; diff --git a/test/evaluators/objects.js b/test/evaluators/objects.js new file mode 100644 index 0000000..6c739a9 --- /dev/null +++ b/test/evaluators/objects.js @@ -0,0 +1,244 @@ +/** + * Created by j.corns on 2/22/17. + */ + +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + describe('#object evaluation', function () { + before(function () { + + this.checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true, + hasSimpleArray: [1, 2, 3] + }; + }); + describe('#objects', function () { + before(function () { + var features = { + // object comparator + "canCheckEqualObject": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "equal", + "value": { a: 1, b: 2 } + } + } + ] + }, + "canCheckAboveObject": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "above", + "value": { some: "string", other: 1234, last: "3o8jsf" } + } + } + ] + }, + "canCheckAboveObjectMixed": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "above", + "value": ["a", "b"] + } + } + ] + }, + "canCheckAboveObjectEqual": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "above", + "value": { a: 1, b: 2 } + } + } + ] + }, + "canCheckBelowObject": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "below", + "value": { some: "string", other: 1234, last: "3o8jsf" } + } + } + ] + }, + "canCheckBelowObjectEqual": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "below", + "value": { a: 1, b: 2 } + } + } + ] + }, + "canCheckBelowObjectMixed": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "below", + "value": ["a", "b"] + } + } + ] + }, + "canCheckShorterObject": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "shorter", + "value": { some: "string", other: 1234, last: "3o8jsf" } + } + } + ] + }, + "canCheckLongerObject": { + "criteria": [ + { + "has": { + "trait": "hasObjectValue", + "comparison": "longer", + "value": { some: "string", other: 1234, last: "3o8jsf" } + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: this.checkData, + features: features, + overrides: {}, + showLogs: false + }); + }); + it('"canCheckEqualObject" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckEqualObject")); + }); + it('"canCheckAboveObject" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckAboveObject")); + }); + it('"canCheckAboveObjectEqual" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckAboveObjectEqual")); + }); + it('"canCheckAboveObjectMixed" should evaluate to true', function () { + assert(!this.bSetup.get("canCheckAboveObjectMixed")); + }); + it('"canCheckBelowObject" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckBelowObject")); + }); + it('"canCheckBelowObjectEqual" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckBelowObjectEqual")); + }); + it('"canCheckBelowObjectMixed" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckBelowObjectMixed")); + }); + it('"canCheckShorterObject" should evaluate to true', function () { + assert(this.bSetup.get("canCheckShorterObject")); + }); + it('"canCheckLongerObject" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckLongerObject")); + }); + + }); + describe('#arrays', function () { + before(function () { + var features = { + "canCheckEqualArray": { + "criteria": [ + { + "has": { + "trait": "hasSimpleArray", + "comparison": "equal", + "value": [1, 2, 3] + } + } + ] + }, + "canCheckAboveArray": { + "criteria": [ + { + "has": { + "trait": "hasSimpleArray", + "comparison": "above", + "value": [7, 8] + } + } + ] + }, + "canCheckAboveArrayMixed": { + "criteria": [ + { + "has": { + "trait": "hasSimpleArray", + "comparison": "above", + "value": { "a": "z", "b": "y" } + } + } + ] + }, + "canCheckBelowArray": { + "criteria": [ + { + "has": { + "trait": "hasSimpleArray", + "comparison": "below", + "value": [5, 6, 7, 8, 9] + } + } + ] + }, + "canCheckBelowArrayMixed": { + "criteria": [ + { + "has": { + "trait": "hasSimpleArray", + "comparison": "below", + "value": { "a": "z", "b": "y" } + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: this.checkData, + features: features, + overrides: {}, + showLogs: false + }); + }); + it('"canCheckEqualArray" should evaluate to true', function () { + assert(!this.bSetup.get("canCheckEqualArray")); + }); + it('"canCheckAboveArray" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckAboveArray")); + }); + it('"canCheckAboveArrayMixed" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckAboveArrayMixed")); + }); + it('"canCheckBelowArray" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckBelowArray")); + }); + it('"canCheckBelowArrayMixed" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckBelowArrayMixed")); + }); + }); + }); +}; diff --git a/test/evaluators/simple.js b/test/evaluators/simple.js new file mode 100644 index 0000000..4e63f8b --- /dev/null +++ b/test/evaluators/simple.js @@ -0,0 +1,75 @@ +/** + * Created by j.corns on 2/22/17. + */ + +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + + describe('#simple evaluation', function () { + before(function () { + + var checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true + }; + var features = { + // always evaluator + "canCheckAlways": { + "criteria": [ + { + "always": false + } + ] + }, + "canCheckHas": { + "criteria": [ + { + "has": { + "trait": "hasStringValue" + } + } + ] + }, + "canCheckHasNot": { + "criteria": [ + { + "has": { + "trait": "doesNotHaveThis" + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: checkData, + features: features, + overrides: {}, + showLogs: false + }); + }); + for (var feature in this.features) { + if (this.features.hasOwnProperty(feature)) { + (function (f) { + it('"' + f + '" should evaluate to boolean', function () { + assert((typeof this.bSetup.get(f) === 'boolean')); + }); + })(feature); + } + } + it('"canCheckAlways" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckAlways")); + }); + it('should pass the "getAll" features', function () { + var allOut = this.bSetup.getAll(); + assert(!!(allOut)); + }); + it('will not error on missing feature', function () { + assert(!this.bSetup.get('noCheckFunction')) + }); + }); +}; diff --git a/test/evaluators/strings.js b/test/evaluators/strings.js new file mode 100644 index 0000000..d414814 --- /dev/null +++ b/test/evaluators/strings.js @@ -0,0 +1,205 @@ +/** + * Created by j.corns on 2/22/17. + */ + +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + describe('#string evaluation', function () { + before(function () { + var checkData = { + id: 4, + hasStringValue: "a string check value" + }, + features = { + // simple string + "canCheckHasString": { + "criteria": [ + { + "has": { + "trait": "hasStringValue" + } + } + ] + }, + "canCheckStringEqual": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": "a string check value" + } + } + ] + }, + "canCheckNullComparison": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "equals", + "value": null + } + } + ] + }, + "similarStrings": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "like", + "value": "A String Check Value" + } + } + ] + }, + "similarStringsNull": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "like", + "value": null + } + } + ] + }, + "stringsBelow": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "below", + "value": "A different string value" + } + } + ] + }, + "stringsBelowNull": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "below", + "value": null + } + } + ] + }, + "stringsAbove": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "above", + "value": "A different string value" + } + } + ] + }, + "stringsAboveNull": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "above", + "value": null + } + } + ] + }, + "stringsLonger": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "longer", + "value": "A different string value" + } + } + ] + }, + "stringsLongerNull": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "longer", + "value": null + } + } + ] + }, + "stringsShorter": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "shorter", + "value": "A different string value" + } + } + ] + }, + "stringsShorterNull": { + "criteria": [ + { + "has": { + "trait": "hasStringValue", + "comparison": "shorter", + "value": null + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: checkData, + features: features, + overrides: {}, + showLogs: false + }); + }); + it('"canCheckHasString" should evaluate to true', function () { + assert(this.bSetup.get("canCheckHasString")); + }); + it('"canCheckStringEqual" should evaluate to true', function () { + assert(this.bSetup.get("canCheckStringEqual")); + }); + it('"canCheckNullComparison" should evaluate to false', function () { + assert(!this.bSetup.get("canCheckNullComparison")); + }); + it('string "like" comparison', function () { + assert(this.bSetup.get('similarStrings')); + }); + it('string "like" comparison against null value', function () { + assert(!this.bSetup.get('similarStringsNull')); + }); + it('string "below" comparison', function () { + assert(!this.bSetup.get('stringsBelow')); + }); + it('string "below" comparison with a null value', function () { + assert(!this.bSetup.get('stringsBelowNull')); + }); + it('string "above" comparison', function () { + assert(this.bSetup.get('stringsAbove')); + }); + it('string "above" comparison with a null value', function () { + assert(this.bSetup.get('stringsAboveNull')); + }); + it('string "longer" comparison', function () { + assert(!this.bSetup.get('stringsLonger')); + }); + it('string "longer" comparison with a null value', function () { + assert(this.bSetup.get('stringsLongerNull')); + }); + it('string "shorter" comparison', function () { + assert(this.bSetup.get('stringsShorter')); + }); + it('string "shorter" comparison with a null value', function () { + assert(!this.bSetup.get('stringsShorterNull')); + }); + }); +}; diff --git a/test/helpers/diagnostics.js b/test/helpers/diagnostics.js new file mode 100644 index 0000000..e5c882d --- /dev/null +++ b/test/helpers/diagnostics.js @@ -0,0 +1,61 @@ +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + describe('Diagnostics', function () { + describe('#exist', function () { + before(function () { + this.d = brie.diagnostics(); + }); + it('should return some value', function () { + assert.ok(this.d); + }); + it('should read features', function () { + assert((typeof this.d.features === "object" && typeof this.d.features.length === "undefined")); + }); + it('should read data', function () { + assert((typeof this.d.data === "object" && typeof this.d.data.length === "undefined")); + }); + it('should have criteria', function () { + assert(!!(this.d)); + }); + it('recognizes data type: string', function () { + assert(this.d.getType("abcd") === 'string'); + }); + it('recognizes data type: integer', function () { + assert(this.d.getType(1) === 'number'); + }); + it('recognizes data type: object', function () { + assert(this.d.getType({ 'a': 1 }) === 'object'); + }); + it('recognizes data type: array', function () { + assert(this.d.getType([1, 2, 3]) === 'array'); + }); + it('recognizes data type: date', function () { + assert(this.d.getType(new Date()) === 'date'); + }); + it('recognizes data type: boolean', function () { + assert(this.d.getType(true) === 'boolean'); + }); + it('recognizes data type: null', function () { + assert(this.d.getType(null) === 'object'); // Yup - null is an object. Read the spec: http://www.ecma-international.org/ecma-262/5.1/#sec-11.4.3 + }); + it('recognizes data type: undefined', function () { + assert(this.d.getType(undefined) === 'undefined'); + }); + }); + + describe('#criteria are executable', function () { + var t_d = brie.diagnostics(); + for (var c in t_d.criteria) { + if (t_d.criteria.hasOwnProperty(c)) { + (function (cta) { + it('criteria "' + cta + '" should be a function', function () { + var k = typeof t_d.criteria[cta] === 'function'; + assert(k); + }); + })(c); + } + } + }); + }); +}; diff --git a/test/helpers/setup.js b/test/helpers/setup.js new file mode 100644 index 0000000..8912783 --- /dev/null +++ b/test/helpers/setup.js @@ -0,0 +1,129 @@ +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + describe('Setup', function () { + before(function () { + + this.checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true + }; + }); + it('should return a live object', function () { + + var bSetup = brie.setup({ + data: this.checkData, + features: { + // always evaluator + "canCheckAlways": { + "criteria": [ + { + "always": false + } + ] + } + }, + overrides: {}, + showLogs: false + }); + assert(!!(bSetup)); + }); + it('should reject invalid feature', function () { + var bSetup = brie.setup({ + data: this.checkData, + features: { "invalidFeature": "no a valid type" }, + overrides: {}, + showLogs: true + }); + assert(bSetup.get("invalidFeature")); + }); + it('should handle disabled features', function () { + + var bSetup = brie.setup({ + data: this.checkData, + features: { + // always evaluator + "canCheckAlways": { + "criteria": [ + { + "always": false + } + ] + }, + "canCheckAnother": { + "enabled": false, + "criteria": [ + { + "always": true + } + ] + } + }, + overrides: {} + }); + assert(bSetup.getAll()); + }); + it('accepts missing criteria', function () { + var bSetup = brie.setup({ + data: this.checkData, + features: { + "acceptsMissingCrits": { + } + }, + overrides: {}, + showLogs: false + }); + assert(!bSetup.get('acceptsMissingCrits')); + }); + it('should return false for simple criteria', function () { + var bSetup = brie.setup({ + data: this.checkData, + features: { + "ignoresMissingCriteria": { "criteria": ["string"] } + }, + overrides: {}, + showLogs: false + }); + assert(bSetup.getAll()); + }); + it('should return false for missing criteria', function () { + var bSetup = brie.setup({ + data: this.checkData, + features: { + "ignoresMissingCriteria": { "criteria": [{}] } + }, + overrides: {}, + showLogs: false + }); + assert(bSetup.getAll()); + }); + it('accepts missing features', function () { + var bSetup = brie.setup({ + data: this.checkData, + overrides: {}, + showLogs: false + }); + assert(!!(bSetup)); + }); + it('accepts missing overrides', function () { + var bSetup = brie.setup({ + data: this.checkData, + features: {}, + showLogs: false + }); + assert(bSetup.getAll()); + }); + it('accepts missing data', function () { + var bSetup = brie.setup({ + overrides: {}, + features: {}, + showLogs: false + }); + assert(!!(bSetup)); + }); + }); +}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..943f840 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--check-leaks +--globals NODE_CONFIG diff --git a/test/reflection/is.js b/test/reflection/is.js new file mode 100644 index 0000000..0d5c0fa --- /dev/null +++ b/test/reflection/is.js @@ -0,0 +1,123 @@ +/** + * Created by j.corns on 2/22/17. + */ + +var assert = require("assert"); +var brie = require('../../lib/brie'); +module.exports = function () { + + describe('#is and type-check', function () { + before(function () { + var checkData = { + id: 123456789, + hasStringValue: "a string check value", + hasNumberValue: 181818, + hasObjectValue: { a: 1, b: 2 }, + hasDateValue: new Date(), + hasBooleanValue: true, + has: { + nested: { + values: { + props: "some value" + } + } + } + }, + features = { + // type evaluator + "canCheckValidType": { + "criteria": [ + { + "is": { + "type": "number", + "trait": "hasNumberValue" + } + } + ] + }, + "canCheckInvalidType": { + "criteria": [ + { + "is": { + "type": "shoe", + "trait": "hasBooleanValue" + } + } + ] + }, + "canCheckNullType": { + "criteria": [ + { + "is": { + "type": null, + "trait": "hasBooleanValue" + } + } + ] + }, + "canCheckUndefinedType": { + "criteria": [ + { + "is": { + "type": undefined, + "trait": "hasBooleanValue" + } + } + ] + }, + "canCheckUndefinedTrait": { + "criteria": [ + { + "is": { + "type": "string" + } + } + ] + }, + "canCheckEmptyIs": { + "criteria": [ + { + "is": { + } + } + ] + }, + "canCheckNestedTraits": { + "criteria": [ + { + "is": { + "type": "string", + "trait": "has.nested.values.props" + } + } + ] + } + }; + this.bSetup = brie.setup({ + data: checkData, + features: features + }); + }); + it('will check that a property is of a known type', function () { + assert(this.bSetup.get("canCheckValidType")); + }); + it('will reject an operation for an unknown type', function () { + assert(!this.bSetup.get("canCheckInvalidType")); + }); + it('will reject an operation for a null type', function () { + assert(!this.bSetup.get("canCheckNullType")); + }); + it('will reject an operation for an undefined type', function () { + assert(!this.bSetup.get("canCheckUndefinedType")); + }); + it('will reject an operation when trait is missing', function () { + assert(!this.bSetup.get("canCheckUndefinedTrait")); + }); + it('will reject an empty "is" block', function () { + assert(!this.bSetup.get("canCheckEmptyIs")); + }); + it('will test nested properties by dot notation', function () { + assert(this.bSetup.get("canCheckNestedTraits")); + }) + }); +}; diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..f087e89 --- /dev/null +++ b/test/test.js @@ -0,0 +1,24 @@ +/** + * Testing Harness for brie + * + * Run as: + * + * @example + * // exit code 0 + * node tests/test.js + * + * Created by j.corns on 3/3/15. + */ + +require('./eslinter'); +require('./helpers/diagnostics')(); +require('./helpers/setup')(); +describe('Execution', function () { + require('./evaluators/complex')(); + require('./evaluators/dates')(); + require('./evaluators/numbers')(); + require('./evaluators/objects')(); + require('./evaluators/simple')(); + require('./evaluators/strings')(); + require('./reflection/is')(); +});