├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── blueprints.config.js
├── flags.js
├── index.es6.js
├── package.json
└── test
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": ["transform-class-properties", "transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "root": true,
5 |
6 | "env": {
7 | // I write for browser
8 | "browser": true,
9 | // in CommonJS
10 | "node": true,
11 | "mocha": true
12 | },
13 |
14 | "extends": "eslint:recommended",
15 |
16 | // plugins let use use custom rules below
17 | "plugins": [
18 | "babel",
19 | "react"
20 | ],
21 |
22 | "ecmaFeatures": {
23 | "jsx": true,
24 | "es6": true,
25 | },
26 |
27 | "rules": {
28 | // 0 = off
29 | // 1 = warn
30 | // 2 = error
31 |
32 | // React specifc rules
33 | "react/jsx-boolean-value": 0,
34 | "react/jsx-closing-bracket-location": 1,
35 | "react/jsx-curly-spacing": [2, "always"],
36 | "react/jsx-indent-props": [1, 2],
37 | "react/jsx-no-undef": 1,
38 | "react/jsx-uses-react": 1,
39 | "react/jsx-uses-vars": 1,
40 | "react/wrap-multilines": 1,
41 | "react/react-in-jsx-scope": 1,
42 | "react/prefer-es6-class": 1,
43 | // no binding functions in render for perf
44 | "react/jsx-no-bind": 1,
45 |
46 | // handle async/await functions correctly
47 | // handle object spread
48 | "babel/object-shorthand": 1,
49 |
50 | // handle potential errors
51 | "brace-style": [2, "1tbs", { "allowSingleLine": true }],
52 | "comma-dangle": [2, "always-multiline"],
53 | "consistent-return": 0,
54 | "indent": [2, 2, {"SwitchCase": 1}],
55 | "max-len": [1, 100, 2, {"ignoreComments": true}],
56 | "no-cond-assign": [2, "always"],
57 | "no-console": 0,
58 | "no-constant-condition": 2,
59 | "no-control-regex": 2,
60 | "no-debugger": 2,
61 | "no-dupe-args": 2,
62 | "no-dupe-keys": 2,
63 | "no-duplicate-case": 2,
64 | "no-empty-character-class": 2,
65 | "no-empty": 2,
66 | "no-ex-assign": 2,
67 | "no-extra-boolean-cast": 2,
68 | "no-extra-semi": 2,
69 | "no-func-assign": 2,
70 | "no-invalid-regexp": 2,
71 | "no-irregular-whitespace": 2,
72 | "no-negated-in-lhs": 2,
73 | "no-obj-calls": 2,
74 | "no-regex-spaces": 2,
75 | "no-sparse-arrays": 2,
76 | "no-unexpected-multiline": 2,
77 | "no-unreachable": 2,
78 | "no-underscore-dangle": 0,
79 | "no-unused-vars": 1,
80 | "quotes": [2, "single"],
81 | "semi": [2, "always"],
82 | "space-after-keywords": [2, "always"],
83 | "space-before-blocks": [2, "always"],
84 | "space-before-function-parentheses": [0, "never"],
85 | "space-in-brackets": [0, "never"],
86 | "space-in-parens": [2, "never"],
87 | "space-return-throw-case": 1,
88 | "space-unary-ops": [1, { "words": true, "nonwords": false }],
89 | "strict": [2, "never"],
90 | "use-isnan": 2,
91 |
92 | // style
93 | "curly": 2,
94 | "dot-location": [2, 'property'],
95 | "dot-notation": 2,
96 | "eqeqeq": 2,
97 | "no-alert": 2,
98 | "no-else-return": 2,
99 | "no-eval": 2,
100 | "no-extra-bind": 2,
101 | "no-fallthrough": 2,
102 | "no-floating-decimal": 2,
103 | "no-implied-eval": 2,
104 | "no-iterator": 2,
105 | "no-labels": 2,
106 | "no-lone-blocks": 2,
107 | "no-loop-func": 2,
108 | "no-multi-spaces": 2,
109 | "no-multi-str": 2,
110 | "no-native-reassign": 2,
111 | "no-new-func": 2,
112 | "no-new-wrappers": 2,
113 | "no-new": 2,
114 | "no-octal-escape": 2,
115 | "no-octal": 2,
116 | "no-proto": 2,
117 | "no-redeclare": 2,
118 | "no-return-assign": 2,
119 | "no-self-compare": 2,
120 | "no-sequences": 2,
121 | "no-throw-literal": 1,
122 | "no-unused-expressions": 2,
123 | "no-useless-call": 2,
124 | "no-useless-concat": 2,
125 | "no-void": 2,
126 | "no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],
127 | "no-with": 2,
128 | "yoda": 2,
129 | "no-delete-var": 2,
130 | "no-shadow-restricted-names": 2,
131 | "no-shadow": [0, { "hoist": "never" }],
132 | "no-undef-init": 2,
133 | "no-undef": 2,
134 | "no-unused-vars": 2,
135 | "callback-return": 2,
136 |
137 | // es6-specific
138 | "prefer-template": 1,
139 | "no-var": 1,
140 | "constructor-super": 2,
141 | "prefer-const": 1,
142 | "prefer-spread": 2,
143 | "no-dupe-class-members": 2,
144 | // commented out; there's a bug where `async` functions throw an error here.
145 | //"generator-star-spacing": [0, { "before": true, "after": false }],
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .eslintrc
3 | .travis.yml
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "4"
5 | - "5"
6 |
7 | script:
8 | - npm run lint
9 | - npm run test
10 |
11 | branches:
12 | except:
13 | - staging
14 |
15 | env:
16 | - CXX=g++-4.8
17 |
18 | addons:
19 | apt:
20 | sources:
21 | - ubuntu-toolchain-r-test
22 | packages:
23 | - g++-4.8
24 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to flags
2 | ====================
3 |
4 | So you want to contribute to flags? Fantastic! Here's a brief overview on
5 | how best to do so.
6 |
7 | ## What to change
8 |
9 | Here's some examples of things you might want to make a pull request for:
10 |
11 | * New features
12 | * Bugfixes
13 | * Inefficient blocks of code
14 |
15 | If you have a more deeply-rooted problem with how the program is built or some
16 | of the stylistic decisions made in the code, it's best to
17 | [create an issue](https://github.com/reddit/flags/issues) before putting
18 | the effort into a pull request. The same goes for new features - it is
19 | best to check the project's direction, existing pull requests, and currently open
20 | and closed issues first.
21 |
22 | ## Style
23 |
24 | * Two spaces, not tabs
25 | * Semicolons are not optional
26 | * All pages should render on the server and the client. The site should be
27 | usable without javascript.
28 | * Review our [style guide](https://github.com/reddit/tree/master/javascript) for
29 | more information.
30 | * Use the .eslint file here to make things go smoothly!
31 |
32 | Look at existing code to get a good feel for the patterns we use. Please run
33 | tests before submitting any pull requests. Instructions for running tests can
34 | be found in the README.
35 |
36 | ## Using Git appropriately
37 |
38 | 1. [Fork the repository](https://github.com/reddit/flags/fork_select) to
39 | your Github account.
40 | 2. Create a *topical branch* - a branch whose name is succint but explains what
41 | you're doing, such as "change-orangered-to-periwinkle"
42 | 3. Make your changes, committing at logical breaks.
43 | 4. Push your branch to your personal account
44 | 5. [Create a pull request](https://help.github.com/articles/using-pull-requests)
45 | 6. Watch for comments or acceptance
46 |
47 | Please make separate branches for unrelated changes!
48 |
49 | ## Licensing
50 |
51 | flags is MIT licensed. See details in the LICENSE file. This is a very permissive
52 | scheme, GPL-compatible but without many of the restrictions of GPL.
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 reddit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | flags
2 | ====
3 |
4 | A simple feature flagging library.
5 |
6 | 100 lines of code, no dependencies. Written in ES6+.
7 |
8 | [](https://travis-ci.org/reddit/flags)
9 |
10 | __flags__ lets you quickly switch features on or off based on a context. This is
11 | useful to test features for specific users (such as flagging on new functionality
12 | in a web application by reading the response context), dark-launching code,
13 | and a/b testing.
14 |
15 | ```javascript
16 | // Import it!
17 | import Flags from '@r/flags';
18 |
19 | // Set up your experiment config!
20 | const config = {
21 | loggedoutSearch: { loggedin: false },
22 | oldDesign: false,
23 | newListingStyle: { users: ['ajacksified'] },
24 | };
25 |
26 | const feature = new Flags(config);
27 |
28 | // Add rules!
29 | feature.addRule('loggedin', function(val) { return this.loggedin === val; });
30 | feature.addRule('users', function(names) { return names.includes(this.username); });
31 |
32 | // Get whatever blob of data you'll use to determine your experiment truthiness
33 | const userData = {
34 | loggedin: true,
35 | username: 'ajacksified',
36 | };
37 |
38 | // For ease of use, build a context-bound flags instance. (Alternatively, you
39 | // could call `feature.on(rule, context)` instead.)
40 | const featureContext = feature.withContext(userData);
41 |
42 | // Build your UI with a Flags context bound to userdata!
43 |
44 | // false (loggedin is true, but the config wants false)
45 | if (featureContext.enabled('loggedoutSearch')) {
46 | searchControl = ;
47 | console.log('show the logged out search');
48 | }
49 |
50 | // false (the config says it's always false)
51 | if (featureContext.enabled('oldDesign')) {
52 | layout = ;
53 | }
54 |
55 | // true (the username is in the users array)
56 | if (featureContext.enabled('newListingStyle')) {
57 | listing = ;
58 | }
59 |
60 | // Get the list of enabled featues:
61 | const enabled = featureContext.allEnabled();
62 | // => ['newListingStyle'];
63 |
64 |
65 | // Or disabled:
66 | const disabled = featureContext.allDisabled();
67 | // => ['loggedoutSearch', 'oldDesign'];
68 | ```
69 |
70 | Rules and Configuration
71 | -----------------------
72 |
73 | __flags__ configuration and rules are very simple:
74 |
75 | * Define a name for your features; such as `'loggedoutSearch'` or `'oldDesign'` as above. These
76 | will be the basis if your config and your feature flags for later on.
77 | * Define the rules upon which your feature will be on or off. Above, we implement
78 | a boolean check (shoes) and an array check (username).
79 | * Check the flag - either by sending it in (`feature.enabled('feature', { data } )`) or
80 | by hanging on to a context-bound instance (
81 | `featureContext = feature.withContext({ data }); featureContext.enabled('feature')`)
82 |
83 | Of note: if a rule is defined in configuration but it is not implemented, it is
84 | assumed to be false.
85 |
86 | Three special "pseudo-rules" implement boolean operators: `and`, `or`, and
87 | `not`, so that rules may stay simple and general and be combined in useful
88 | ways. For example:
89 |
90 | ```javascript
91 | const config = {
92 | friend: {
93 | and: [
94 | { or: [ { type: 'cyborg' }, { type: 'human' } ] },
95 | { not: { role: 'supervillain' } }
96 | ]
97 | }
98 | };
99 | ```
100 |
101 | You can also parse large objects to handle things such as environment variables.
102 | To do so, you should have configuration in the format `feature_name`. `feature_`
103 | will be stripped from the key and lowercased:
104 |
105 | ```javascript
106 | import Flags from flags;
107 |
108 | const config = Flags.parseConfig({
109 | something: 'wut',
110 | feature_flippers: { names: ['bob', 'jane'] }
111 | });
112 |
113 | // => { flippers: { names: ['bob', 'jane'] } };
114 | ```
115 |
116 | You can supply your own function, too, and customize both key and value. In
117 | this example, we'll expect things to start with `f_` instead of `feature_`.
118 |
119 | ```javascript
120 | import Flags from flags;
121 |
122 | const config = Flags.parseConfig({
123 | something: 'wut',
124 | f_flippers: { names: ['bob', 'jane'] }
125 | }, function(key, val) {
126 | if (key.indexOf('f_') === -1) { return; }
127 | return { key: key.slice(2), val: val };
128 | });
129 |
130 | // => { flippers: { names: ['bob', 'jane'] } };
131 | ```
132 |
133 | You can also retrieve a list of all currently enabled or disabled rules for a
134 | given context by calling `feature.allEnabled` or `feature.allDisabled`. These
135 | can be called with a context passed in, or else will use a bound context.
136 |
137 | Sometimes, for testing, it's nice to clone an instance that doesn't use the
138 | original rules and configuration by reference. You can call
139 | `flagsinstance.clone()` to return a new flags instance with shallow copies of
140 | the config, rules, and context.
141 |
142 | Development
143 | -----------
144 |
145 | __flags__ is an ES6 library. Take a look at the `.babelrc` file to see what
146 | presets we're using. To import it in ES5, use
147 | `var Flags = require ('@r/flags').default;`.
148 |
149 | To get started:
150 |
151 | * With node installed,
152 | * Fork __flags__
153 | * Clone your fork to your machine
154 | * Run `npm install` to install dev dependencies (there are no regular
155 | dependencies; dependencies are for testing and running.)
156 | * Write features, and run `npm test` and `npm run lint`. Feature changes should
157 | have tests! For ease of use, install `eslint` for your favorite editor.
158 | * Check out `CONTRIBUTING.md` for more detailed info.
159 |
160 | flags is MIT licensed. copyright 2016 reddit. See LICENSE for more info.
161 |
--------------------------------------------------------------------------------
/blueprints.config.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | name: 'apiClient',
3 | webpack: {
4 | entry: {
5 | flags: './index.es6.js',
6 | },
7 | output: {
8 | library: '[name].js',
9 | libraryTarget: 'umd',
10 | },
11 | resolve: {
12 | generator: 'npm-and-modules',
13 | extensions: ['', '.js', '.jsx', '.es6.js', '.json'],
14 | },
15 | loaders: [
16 | 'esnextreact',
17 | 'json',
18 | ],
19 | plugins: [
20 | 'production-loaders',
21 | 'set-node-env',
22 | 'abort-if-errors',
23 | 'minify-and-treeshake',
24 | ],
25 | externals: 'node-modules',
26 | },
27 | }];
28 |
--------------------------------------------------------------------------------
/flags.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["flags.js"]=t():e["flags.js"]=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=0)}([function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t {
12 | const parsed = parsefn(key, config[key]);
13 |
14 | if (parsed) {
15 | o[parsed.key] = parsed.val;
16 | }
17 |
18 | return o;
19 | }, {});
20 | }
21 |
22 | static parse (key, val) {
23 | const k = key.toLowerCase();
24 | if (k.indexOf(DEFAULT_FEATURE_PARSE_PREFIX) === -1) { return; }
25 |
26 | return { key: k.slice(DEFAULT_FEATURE_PARSE_PREFIX.length), val };
27 | }
28 |
29 | // Build a new instance. In most cases, you only pass in config that sets
30 | // experiments up; sometimes, you may prefer to also pass rules in rather
31 | // than calling addRule. Passing in ctx is really only to be used internally
32 | // by withContext.
33 | constructor (featureConfig, rules={}, ctx={}) {
34 | this.config = featureConfig;
35 | this.rules = rules;
36 | this.ctx = ctx;
37 | }
38 |
39 | // Add a new rule
40 | addRule (name, fn) {
41 | this.rules[name] = fn;
42 | }
43 |
44 | testRule (config, rule, ctx) {
45 | // If there's no rule, check for boolean operator pseudo-rules.
46 | if (!this.rules[rule]) {
47 | return this.tryBooleanOps(config, rule, ctx);
48 | }
49 |
50 | // If there is a rule, return if it failed.
51 | return this.rules[rule].call(ctx, config[rule]);
52 | };
53 |
54 | // Check for boolean operator pseudo-rules.
55 | tryBooleanOps (config, rule, ctx) {
56 | if (rule === 'or') {
57 | return config[rule].some(subConfig =>
58 | Object.keys(subConfig).some(subRule => this.testRule(subConfig, subRule, ctx))
59 | )
60 | }
61 |
62 | if (rule === 'and') {
63 | return config[rule].every(subConfig =>
64 | Object.keys(subConfig).some(subRule => this.testRule(subConfig, subRule, ctx))
65 | )
66 | }
67 |
68 | if (rule === 'not') {
69 | const subConfig = config[rule];
70 | return !(Object.keys(subConfig).some(subRule => this.testRule(subConfig, subRule, ctx)));
71 | }
72 |
73 | // Otherwise, return false.
74 | return false;
75 | }
76 |
77 | // Check if your flags are on a thing
78 | enabled (name, ctx=this.ctx) {
79 | const config = this.config[name];
80 |
81 | // If there's no config for the feature we're checking for, assume false.
82 | if (!config) { return false; }
83 |
84 | // If the flag is a boolean, just return it.
85 | if (config === true || config === false) {
86 | return config;
87 | }
88 |
89 | // Otherwise, get the list of rules from the config.
90 | const rules = Object.keys(config);
91 |
92 | // Check if any of the rules fail. Use `find`, which exits as immiediately
93 | // as possible.
94 | const pass = rules.some((r) => this.testRule(config, r, ctx));
95 |
96 | // Return whether any of the rules failed
97 | return pass;
98 | }
99 |
100 | // Loop through all configured features and return a string array of what
101 | // features _would_ return true for a given context (or are disabled if
102 | // enabled = false.)
103 | allEnabled (ctx=this.ctx, enabled=true) {
104 | return Object.keys(this.config).filter((configName) => {
105 | return this.enabled(configName, ctx) === enabled;
106 | });
107 | }
108 |
109 | // Loop through all configured features and return a string array of what
110 | // features _would not_ return true for a given context.
111 | allDisabled (ctx=this.ctx) {
112 | return this.allEnabled(ctx, false);
113 | }
114 |
115 | // Create a new, context-bound flags instance for easy calling later on.
116 | withContext (ctx) {
117 | return new Flags(this.config, this.rules, ctx);
118 | }
119 |
120 | clone (config=this.config, rules=this.rules, ctx=this.ctx) {
121 | return new Flags({ ...config}, { ...rules }, { ...ctx });
122 | }
123 | }
124 |
125 | export default Flags;
126 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@r/flags",
3 | "version": "1.5.0",
4 | "description": "config goes in, feature-enabled goes out",
5 | "main": "flags.js",
6 | "scripts": {
7 | "watch": "blueprints -w",
8 | "build": "blueprints",
9 | "test": "mocha --compilers js:babel-register",
10 | "lint": "eslint src"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/reddit/flags.git"
15 | },
16 | "keywords": [
17 | "feature",
18 | "flag",
19 | "toggle",
20 | "switch",
21 | "ab",
22 | "a/b",
23 | "test"
24 | ],
25 | "author": "Jack Lawson ",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/reddit/flags/issues"
29 | },
30 | "homepage": "https://github.com/reddit/feet#readme",
31 | "devDependencies": {
32 | "@r/build": "^0.6.0",
33 | "babel-core": "^6.9.0",
34 | "babel-eslint": "^6.0.4",
35 | "babel-plugin-syntax-trailing-function-commas": "^6.8.0",
36 | "babel-plugin-transform-async-to-generator": "^6.8.0",
37 | "babel-plugin-transform-class-properties": "^6.8.0",
38 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
39 | "babel-polyfill": "6.8.0",
40 | "babel-preset-es2015": "^6.5.0",
41 | "babel-preset-react": "^6.5.0",
42 | "babel-register": "6.5.1",
43 | "chai": "^3.5.0",
44 | "eslint": "^1.10.3",
45 | "eslint-plugin-babel": "^3.1.0",
46 | "mocha": "^2.4.5",
47 | "sinon": "^1.17.3",
48 | "sinon-chai": "^2.8.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-expressions:0 */
2 | import chai from 'chai';
3 | import sinon from 'sinon';
4 | import sinonChai from 'sinon-chai';
5 |
6 | import Feet from '../index.es6.js';
7 |
8 | const expect = chai.expect;
9 | chai.use(sinonChai);
10 |
11 | describe('Feet', () => {
12 | describe('constructor', () => {
13 | it('exists', () => {
14 | expect(Feet).to.not.be.undefined;
15 | });
16 |
17 | it('loads with passed-in config', () => {
18 | const config = { test: false };
19 | const feet = new Feet(config);
20 |
21 | expect(feet.config).to.equal(config);
22 | });
23 |
24 | it('loads with passed-in config and rules', () => {
25 | const config = { test: false };
26 | const rules = { rule () { }};
27 |
28 | const feet = new Feet(config, rules);
29 |
30 | expect(feet.rules).to.equal(rules);
31 | });
32 |
33 | it('loads with passed-in config and rules and conext', () => {
34 | const config = { test: { rule: true } };
35 | const rules = { rule: sinon.spy() };
36 | const ctx = { name: 'hamster' };
37 |
38 | const feet = new Feet(config, rules, ctx);
39 |
40 | expect(feet.ctx).to.equal(ctx);
41 | feet.enabled('test');
42 | expect(rules.rule.thisValues[0]).to.equal(ctx);
43 | });
44 | });
45 |
46 | describe('adding rules', () => {
47 | it('adds a rule', function() {
48 | const config = { test: false };
49 | const feet = new Feet(config);
50 | const rule = sinon.spy();
51 | feet.addRule('shoes', rule);
52 |
53 | expect(feet.rules.shoes).to.equal(rule);
54 | });
55 | });
56 |
57 | describe('checking feature', () => {
58 | it('returns false with no rules defined', () => {
59 | const config = { test: { rule: true } };
60 | const feet = new Feet(config);
61 | const enabled = feet.enabled('test');
62 | expect(enabled).to.be.false;
63 | });
64 |
65 | it('returns false when a context does not pass a rule', () => {
66 | const config = { test: { rule: true } };
67 | const rules = { rule () { return false; }};
68 | const ctx = { name: 'hamster' };
69 |
70 | const feet = new Feet(config, rules, ctx);
71 |
72 | const enabled = feet.enabled('test');
73 | expect(enabled).to.be.false;
74 | });
75 |
76 | it('returns true when a passes a rule', () => {
77 | const config = { test: { rule: true } };
78 | const rules = { rule () { return true; }};
79 | const ctx = { name: 'hamster' };
80 |
81 | const feet = new Feet(config, rules, ctx);
82 |
83 | const enabled = feet.enabled('test');
84 | expect(enabled).to.be.true;
85 | });
86 |
87 | it('returns true when a passes at least one rule', () => {
88 | const config = {
89 | test: {
90 | frule: true,
91 | trule: true,
92 | },
93 | };
94 |
95 | const rules = {
96 | frule () { },
97 | trule () { return true; },
98 | };
99 |
100 | const ctx = { name: 'hamster' };
101 |
102 | const feet = new Feet(config, rules, ctx);
103 |
104 | const enabled = feet.enabled('test');
105 | expect(enabled).to.be.true;
106 | });
107 |
108 | it('returns a list of all enabled features', () => {
109 | const config = {
110 | test1: true,
111 | test2: true,
112 | test3: false,
113 | };
114 |
115 | const feet = new Feet(config);
116 | const enabled = feet.allEnabled();
117 | expect(enabled).to.deep.equal(['test1', 'test2']);
118 | });
119 |
120 | it('returns a list of all enabled features for a given context', () => {
121 | const config = {
122 | test1: { isHamster: true },
123 | test2: { isHamster: true },
124 | test3: { isHamster: false },
125 | };
126 |
127 | const rules = {
128 | isHamster (b) { return (this.name === 'hamster') === b; },
129 | };
130 |
131 | const context = { name: 'hamster' };
132 |
133 | const feet = new Feet(config, rules, context);
134 | const enabled = feet.allEnabled();
135 | expect(enabled).to.deep.equal(['test1', 'test2']);
136 | });
137 |
138 | it('returns a list of all disabled features', () => {
139 | const config = {
140 | test1: true,
141 | test2: true,
142 | test3: false,
143 | };
144 |
145 | const feet = new Feet(config);
146 | const enabled = feet.allDisabled();
147 | expect(enabled).to.deep.equal(['test3']);
148 | });
149 |
150 | it('returns a list of all disabled features for a given context', () => {
151 | const config = {
152 | test1: { isHamster: true },
153 | test2: { isHamster: true },
154 | test3: { isHamster: false },
155 | };
156 |
157 | const rules = {
158 | isHamster (b) { return (this.name === 'hamster') === b; },
159 | };
160 |
161 | const context = { name: 'hamster' };
162 |
163 | const feet = new Feet(config, rules, context);
164 | const enabled = feet.allDisabled();
165 | expect(enabled).to.deep.equal(['test3']);
166 | });
167 |
168 | it('supports explicit "or" rules', () => {
169 | const config = {
170 | test1: {
171 | or: [
172 | { isHamster: true },
173 | { isDog: true },
174 | ]
175 | },
176 | test2: {
177 | or: [
178 | { isHamster: true },
179 | { isDog: false },
180 | ]
181 | },
182 | test3: {
183 | or: [
184 | { isHamster: false },
185 | { isDog: false }
186 | ]
187 | },
188 | test4: {
189 | or: [
190 | { isHamster: false },
191 | { isDog: true },
192 | ]
193 | },
194 | };
195 |
196 | const rules = {
197 | isHamster (b) { return (this.name === 'hamster') === b; },
198 | isDog (b) { return (this.name === 'dog') === b; },
199 | };
200 |
201 | const context = { name: 'hamster' };
202 |
203 | const feet = new Feet(config, rules, context);
204 | const enabled = feet.allEnabled();
205 | expect(enabled).to.deep.equal(['test1', 'test2', 'test3']);
206 | });
207 |
208 | it('supports "and" rules', () => {
209 | const config = {
210 | test1: {
211 | and: [
212 | { isHamster: true },
213 | { isDog: true },
214 | ]
215 | },
216 | test2: {
217 | and: [
218 | { isHamster: true },
219 | { isDog: false },
220 | ]
221 | },
222 | test3: {
223 | and: [
224 | { isHamster: false },
225 | { isDog: false },
226 | ]
227 | },
228 | test4: {
229 | and: [
230 | { isHamster: false },
231 | { isDog: true },
232 | ]
233 | },
234 | };
235 |
236 | const rules = {
237 | isHamster (b) { return (this.name === 'hamster') === b; },
238 | isDog (b) { return (this.name === 'dog') === b; },
239 | };
240 |
241 | const context = { name: 'hamster' };
242 |
243 | const feet = new Feet(config, rules, context);
244 | const enabled = feet.allEnabled();
245 | expect(enabled).to.deep.equal(['test2']);
246 | });
247 |
248 | it('supports "not" rules', () => {
249 | const config = {
250 | test1: { not: { isHamster: true } },
251 | test2: { not: { isHamster: false } },
252 | test3: {
253 | not: {
254 | isHamster: false,
255 | isDog: false
256 | },
257 | },
258 | };
259 |
260 | const rules = {
261 | isHamster (b) { return (this.name === 'hamster') === b; },
262 | isDog (b) { return (this.name === 'dog') === b; },
263 | };
264 |
265 | const context = { name: 'hamster' };
266 |
267 | const feet = new Feet(config, rules, context);
268 | const enabled = feet.allEnabled();
269 | expect(enabled).to.deep.equal(['test2']);
270 | });
271 |
272 | it('supports nested boolean operator rules', () => {
273 | const config = {
274 | test1: {
275 | and: [
276 | { isHamster: true },
277 | { not: { isDog: true } },
278 | ]
279 | },
280 | test2: {
281 | and: [
282 | {
283 | or: [
284 | { isHamster: true },
285 | { isHamster: false },
286 | ],
287 | and: [
288 | { isHamster: true },
289 | { isDog: false },
290 | ],
291 | }
292 | ]
293 | },
294 | };
295 |
296 | const rules = {
297 | isHamster (b) { return (this.name === 'hamster') === b; },
298 | isDog (b) { return (this.name === 'dog') === b; },
299 | };
300 |
301 | const context = { name: 'hamster' };
302 |
303 | const feet = new Feet(config, rules, context);
304 | const enabled = feet.allEnabled();
305 | expect(enabled).to.deep.equal(['test1', 'test2']);
306 | });
307 | });
308 |
309 | describe('static helpers', () => {
310 | it('parses config with defaults', () => {
311 | const config = {
312 | FEATURE_THING: 1,
313 | FEATURE_OTHER_THING: 2,
314 | not_a_feature: 3,
315 | };
316 |
317 | const expectedConfig = {
318 | thing: 1,
319 | other_thing: 2,
320 | };
321 |
322 | expect(Feet.parseConfig(config)).deep.equal(expectedConfig);
323 | });
324 |
325 | it('parses config with custom fn', () => {
326 | const config = {
327 | f_thing: 1,
328 | f_other_thing: 2,
329 | not_a_feature: 3,
330 | };
331 |
332 | const expectedConfig = {
333 | thing: 1,
334 | other_thing: 2,
335 | };
336 |
337 | const customparse = function(k, v) {
338 | if (k.indexOf('f_') === -1) { return; }
339 | return { key: k.slice(2), val: v };
340 | };
341 |
342 | expect(Feet.parseConfig(config, customparse)).deep.equal(expectedConfig);
343 | });
344 | });
345 |
346 | describe('cloning', () => {
347 | it('can clone itself by shallow copy of config, rules, and ctx', () => {
348 | const config = { test: { rule: true } };
349 | const rules = { rule: sinon.spy() };
350 | const ctx = { name: 'hamster' };
351 |
352 | const feet = new Feet(config, rules, ctx);
353 | const newfeet = feet.clone();
354 |
355 | expect(feet.config).to.not.equal(newfeet.config);
356 | expect(feet.config).to.deep.equal(newfeet.config);
357 |
358 | expect(feet.rules).to.not.equal(newfeet.rules);
359 | expect(feet.rules).to.deep.equal(newfeet.rules);
360 |
361 | expect(feet.ctx).to.not.equal(newfeet.ctx);
362 | expect(feet.ctx).to.deep.equal(newfeet.ctx);
363 | });
364 | });
365 | });
366 |
--------------------------------------------------------------------------------