Coverage

94%
173
164
9

/Users/kcambridge/Work/reflip/index.js

98%
68
67
1
LineHitsSource
11var
2 _ = require('lodash'),
3 assert = require('assert'),
4 events = require('events'),
5 http = require('http'),
6 P = require('bluebird'),
7 util = require('util'),
8 Feature = require('./lib/feature'),
9 FileAdapter = require('./lib/file'),
10 RedisAdapter = require('./lib/redis')
11 ;
12
131var Reflip = module.exports = function(opts)
14{
159 events.EventEmitter.call(this);
16
179 assert(opts, 'You must pass options to the Reflip constructor');
189 assert(opts.storage || opts.features, 'You must pass either storage options or a pre-set feature object');
19
2010 if (opts.features) this.features = opts.features;
219 if (_.has(opts, 'default')) this.default = opts.default;
229 if (opts.httpcode) this.httpcode = opts.httpcode;
239 this.exportName = opts.exportName || 'check';
24
259 this.features = {};
26
279 if (opts.storage)
28 {
298 var self = this;
308 this.storage = opts.storage;
3117 this.storage.on('update', function(f) { self.update(f); });
328 this.refresh();
33 }
34};
351util.inherits(Reflip, events.EventEmitter);
36
371Reflip.FileAdapter = FileAdapter;
381Reflip.RedisAdapter = RedisAdapter;
391Reflip.Feature = Feature;
40
411Reflip.prototype.storage = null;
421Reflip.prototype.features = null;
431Reflip.prototype.default = false;
441Reflip.prototype.httpcode = 404;
45
461Reflip.prototype.flip = function()
47{
484 var self = this;
494 var defEnabled = this.default;
50
514 function middleware(request, response, next)
52 {
533 request[self.exportName] = function(name)
54 {
553 if (name == null)
56 {
571 return _.transform(self.features, function(features, v, k)
58 {
594 features[k] = v.check(request);
60 });
61 }
62
632 if (!_.has(self.features, name))
641 return self.default;
65
661 return self.features[name].check(request);
67 };
683 next();
69 }
70
714 return middleware;
72};
73
741Reflip.prototype.gate = function gate(name, failureHandler)
75{
765 var self = this;
775 assert(name && name.length, 'You must provide a feature name');
78
794 var isCustomResponse = typeof failureHandler == 'function';
804 function gateFunc(request, response, next)
81 {
823 if (request.check(name))
831 return next();
84
852 if (!isCustomResponse)
861 return response.send(self.httpcode, http.STATUS_CODES[self.httpcode]);
87
881 failureHandler(request, response);
89 }
90
914 return gateFunc;
92};
93
941Reflip.prototype.register = function register(name, func)
95{
963 var feat;
973 if (name instanceof Feature)
981 feat = name;
99 else
100 {
1012 feat = new Feature(
102 {
103 name: name,
104 type: 'custom',
105 enabled: true,
106 checker: func
107 });
108 }
109
1103 this.features[feat.name] = feat;
111};
112
1131Reflip.prototype.refresh = function refresh()
114{
1158 if (!this.storage)
1160 return P.cast(true);
117
1188 var self = this;
1198 this.emit('refreshing');
1208 self.storage.refresh();
121};
122
1231Reflip.prototype.update = function update(features)
124{
1259 var self = this;
126
1279 _.each(features, function(def)
128 {
12935 var feature = new Feature(def);
13035 self.features[feature.name] = feature;
131 });
1329 self.emit('ready');
133};
134
1351Reflip.prototype.shutdown = function shutdown()
136{
1372 if (this.storage)
1382 this.storage.close();
139};
140

/Users/kcambridge/Work/reflip/lib/feature.js

100%
26
26
0
LineHitsSource
11var
2 _ = require('lodash'),
3 assert = require('assert')
4 ;
5
61var Feature = module.exports = function Feature(opts)
7{
838 assert(opts && (typeof opts == 'object'), 'you must pass an options object to the Feature constructor');
9
1038 this.name = opts.name;
1138 this.type = opts.type;
1238 this.enabled = opts.enabled;
1338 this.chance = _.has(opts, 'chance') ? opts.chance : 100;
1438 this.checker = opts.checker;
1538 this.groups = opts.groups;
16};
17
181Feature.prototype.name = 'aardvarks'; // name
191Feature.prototype.type = 'boolean'; // one of boolean, grouped, metered, custom
201Feature.prototype.enabled = false; // on/off by default
211Feature.prototype.chance = 100; // percentage; consulted if this is a metered feature
221Feature.prototype.checker = null; // user-supplied check function
231Feature.prototype.groups = null; // used for a/b testing stuff
24
251Feature.prototype.check = function check(request)
26{
278 if (!this.enabled)
283 return false;
29
305 switch (this.type)
31 {
32 case 'custom':
331 return this.checker(request);
34
35 case 'boolean':
362 return true;
37
38 case 'metered':
391 return (Math.random() * 100 > this.chance);
40
41 case 'grouped':
421 return this.checkGroup(request);
43 }
44};
45
461Feature.prototype.checkGroup = function checkGroup(request)
47{
481 var idx = _.random(0, this.groups.length - 1);
491 return this.groups[idx];
50
51 // TODO store it in the session?
52};
53
54

/Users/kcambridge/Work/reflip/lib/file.js

93%
31
29
2
LineHitsSource
11var
2 assert = require('assert'),
3 events = require('events'),
4 fs = require('fs'),
5 P = require('bluebird'),
6 path = require('path'),
7 util = require('util')
8 ;
9
101var FileAdapter = module.exports = function FileAdapter(opts)
11{
1212 assert(opts && (typeof opts == 'object'), 'you must pass an options object to the RedisAdapter constructor');
1311 assert(opts.filename, 'you must specify a file to read from');
14
1510 this.filename = opts.filename;
16
1710 var self = this;
1810 this.watcher = fs.watch(this.filename, function(event, fname)
19 {
202 self.refresh();
21 });
22};
231util.inherits(FileAdapter, events.EventEmitter);
24
251FileAdapter.prototype.filename = null;
26
271FileAdapter.prototype.refresh = function refresh()
28{
298 var self = this;
30
318 this.read()
32 .then(function(response)
33 {
348 self.emit('update', response.features);
35 },
36 function(err)
37 {
380 self.emit('error', err);
39 });
40};
41
421FileAdapter.prototype.read = function read()
43{
4411 var deferred = P.pending();
45
4611 fs.readFile(this.filename, function(err, data)
47 {
4811 if (err) return deferred.reject(err);
4911 parseJSON(data, function(err, value)
50 {
5111 if (err) return deferred.reject(err);
5211 deferred.fulfill(value);
53 });
54 });
55
5611 return deferred.promise;
57};
58
591FileAdapter.prototype.close = function close()
60{
613 this.watcher.close();
62};
63
641function parseJSON(data, callback)
65{
6611 var value;
6711 try
68 {
6911 value = JSON.parse(data);
70 }
71 catch (err)
72 {
730 return callback(err);
74 }
7511 callback(null, value);
76}
77

/Users/kcambridge/Work/reflip/lib/redis.js

87%
48
42
6
LineHitsSource
11var
2 _ = require('lodash'),
3 assert = require('assert'),
4 events = require('events'),
5 P = require('bluebird'),
6 redis = require('redis'),
7 util = require('util')
8 ;
9
101var RedisAdapter = module.exports = function RedisAdapter(opts)
11{
129 assert(opts && (typeof opts == 'object'), 'you must pass an options object to the RedisAdapter constructor');
138 assert(opts.client || (opts.host && opts.port), 'you must pass either a redis client or options sufficient to create one');
14
157 if (opts.ttl) this.ttl = opts.ttl;
169 if (opts.namespace) this.namespace = opts.namespace;
17
187 if (opts.client)
196 this.client = opts.client;
20 else
211 this.client = redis.createClient(opts.port, opts.host);
22
23};
241util.inherits(RedisAdapter, events.EventEmitter);
25
261RedisAdapter.prototype.client = null;
271RedisAdapter.prototype.namespace = 'reflip:';
281RedisAdapter.prototype.ttl = 5 * 60 * 1000; // 5 minutes
291RedisAdapter.prototype.refreshTimer = null;
30
311RedisAdapter.prototype.makeKey = function makeKey(base)
32{
3321 return this.namespace + base;
34};
35
361RedisAdapter.prototype.refresh = function refresh()
37{
382 var self = this;
39
402 self.read()
41 .then(function(response)
42 {
432 self.emit('update', response.features);
44 }, function(err)
45 {
460 console.log(err);
47 });
48};
49
501RedisAdapter.prototype.read = function read()
51{
524 var self = this,
53 deferred = P.pending(),
54 result = { features: [] };
55
564 var chain = self.client.multi();
574 chain.smembers(self.makeKey('features'));
584 chain.get(self.makeKey('ttl'));
59
604 chain.exec(function(err, replies)
61 {
624 if (err) return deferred.reject(err);
63
644 if (!_.isNull(replies[1]))
653 result.ttl = parseInt(replies[1], 10);
66 else
671 result.ttl = 0;
684 self.ttl = result.ttl;
69
704 if (self.ttl)
713 self.refreshTimer = setInterval(self.refresh.bind(self), self.ttl);
72 else
731 self.refreshTimer = null;
74
754 var chain2 = self.client.multi();
764 _.each(replies[0], function(f)
77 {
7812 chain2.hgetall(self.makeKey(f));
79 });
80
814 chain2.exec(function(err, hashes)
82 {
834 result.features = hashes;
844 deferred.fulfill(result);
85 });
86 });
87
884 return deferred.promise;
89};
90
911RedisAdapter.prototype.close = function close()
92{
930 this.client.close();
940 this.client = null;
950 if (this.refreshTimer)
96 {
970 clearInterval(this.refreshTimer);
980 this.refreshTimer = null;
99 }
100};
101