Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Store outline

  • Loading branch information...
commit 11c782b835ac58f9b236a0abfd76e449f14fd182 1 parent 76f5ce3
@hueniverse hueniverse authored
View
18 .gitignore
@@ -0,0 +1,18 @@
+.idea
+*.iml
+npm-debug.log
+dump.rdb
+node_modules
+results.tap
+results.xml
+npm-shrinkwrap.json
+config.json
+.DS_Store
+*/.DS_Store
+*/*/.DS_Store
+._*
+*/._*
+*/*/._*
+coverage.*
+lib-cov
+
View
5 .travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+
+node_js:
+ - 0.10
+
View
0  README.md 100644 → 100755
File mode changed
View
134 lib/index.js 100644 → 100755
@@ -0,0 +1,134 @@
+// Load modules
+
+var Hapi = require('hapi');
+var Store = require('./store');
+
+
+// Declare internals
+
+var internals = {};
+
+
+exports.register = function (plugin, options, next) {
+
+ options = options || {};
+
+ var prefix = options.prefix || '';
+ Hapi.utils.assert(!prefix || prefix[0] === '/', 'Path prefix must begin with /');
+
+ var key = {
+ method: 'GET',
+ path: prefix + '/key/{id}/{key*}',
+ config: {
+ handler: internals.keyHandler(),
+ validate: {
+ path: {
+ id: Hapi.types.String()
+ .min(5).max(30).alphanum()
+ .description('Configuration client identifier issued by the confidence API'),
+ key: Hapi.types.String()
+ .description('Full path to the requested key')
+ },
+ query: {
+ depth: Hapi.types.Number().min(1)
+ .description('Number of nested keys to retrieve')
+ }
+ },
+ description: 'Retrieve a key value or a key tree for given client identifier'
+ }
+ };
+
+ var id = {
+ method: 'POST',
+ path: prefix + '/id',
+ config: {
+ handler: internals.idHandler(),
+ validate: {
+ payload: {
+ platform: Hapi.types.String().valid('mweb', 'ios', 'android').required()
+ .description('Platform identifier'),
+ version: Hapi.types.String()
+ .description('Application version string')
+ }
+ },
+ description: 'Issue a unique client identifier for given client profile'
+ }
+ };
+
+ var read = {
+ method: 'GET',
+ path: prefix + '/node/{key*}',
+ config: {
+ handler: internals.readHandler(),
+ validate: {
+ path: {
+ key: Hapi.types.String()
+ .description('Full path to the requested key')
+ },
+ query: {
+ }
+ },
+ description: 'Retrieve a raw key value'
+ }
+ };
+
+ var write = {
+ method: 'POST',
+ path: prefix + '/node/{key*}',
+ config: {
+ handler: internals.writeHandler(),
+ validate: {
+ path: {
+ key: Hapi.types.String()
+ .description('Full path to the key being modified')
+ },
+ payload: {
+ }
+ },
+ description: 'Set a raw key value'
+ }
+ };
+
+ plugin.select('api').route([id, key]);
+ plugin.select('admin').route([read, write]);
+
+ return next();
+};
+
+
+internals.keyHandler = function () {
+
+ return function () {
+
+ this.reply('ok');
+ };
+};
+
+
+internals.idHandler = function () {
+
+ return function () {
+
+ this.reply('ok');
+ };
+};
+
+
+internals.readHandler = function () {
+
+ return function () {
+
+ this.reply('ok');
+ };
+};
+
+
+internals.writeHandler = function () {
+
+ return function () {
+
+ this.reply('ok');
+ };
+};
+
+
View
264 lib/store.js
@@ -0,0 +1,264 @@
+// Load modules
+
+var Hapi = require('hapi');
+
+
+// Declare internals
+
+var internals = {};
+
+
+exports = module.exports = internals.Store = function () {
+
+ this._tree = {};
+};
+
+
+internals.Store.prototype.load = function (tree) {
+
+ var err = internals.validate(tree);
+ if (err) {
+ return err;
+ }
+
+ this._tree = tree;
+};
+
+
+// Get a filtered value
+
+internals.Store.prototype.get = function (key, criteria, depth) {
+
+ // Parse key
+
+ var path = [];
+ if (key !== '/') {
+ var invalid = key.replace(/\/(\w+)/g, function ($0, $1) {
+
+ path.push($1);
+ return '';
+ });
+
+ if (invalid) {
+ return null;
+ }
+ }
+
+ // Find tree node
+
+ var node = internals.filter(this._tree, criteria);
+ for (var i = 0, il = path.length; i < il && node; ++i) {
+ if (typeof node !== 'object') {
+ node = null;
+ break;
+ }
+
+ node = internals.filter(node[path[i]], criteria);
+ }
+
+ return internals.walk(node, criteria, depth);
+};
+
+
+// Read raw value
+
+internals.Store.prototype.read = function (key) {
+
+ // Parse key
+
+ var path = [];
+ if (key !== '/') {
+ var invalid = key.replace(/\/((?:\$default)|(?:\$filter)|(?:\w+))/g, function ($0, $1) {
+
+ path.push($1);
+ return '';
+ });
+
+ if (invalid) {
+ return null;
+ }
+ }
+
+ // Find tree node
+
+ var node = this._tree;
+ for (var i = 0, il = path.length; i < il && node; ++i) {
+ node = node[path[i]];
+ }
+
+ return node || null; // Override undefined
+};
+
+
+internals.Store.prototype.addNode = function (path, key, values) {
+
+
+};
+
+
+// Validate tree structure
+
+internals.validate = function (node, path) {
+
+ path = path || '';
+
+ var error = function (reason) {
+
+ var e = new Error(reason);
+ e.path = path || '/';
+ return e;
+ };
+
+ // NULL or undefined
+
+ if (node === null ||
+ node === undefined) {
+
+ return error('Node cannot be null or undefined');
+ }
+
+ // Valid value
+
+ if (typeof node !== 'object') {
+ return null;
+ }
+
+ // Invalid object
+
+ if (node instanceof Array ||
+ node instanceof Error ||
+ node instanceof Date ||
+ node instanceof RegExp) {
+
+ return error('Invalid node object type');
+ }
+
+ // Empty object
+
+ var keys = Object.keys(node);
+ if (keys.length === 0) {
+ return error('Node cannot be empty');
+ }
+
+ // Invalid keys
+
+ var found = {};
+ for (var i = 0, il = keys.length; i < il; ++i) {
+ var key = keys[i];
+ if (key[0] === '$') {
+ if (key === '$filter') {
+ found.filter = true;
+ var filter = node[key];
+ if (!filter ||
+ typeof filter !== 'string' ||
+ !filter.match(/\w+/)) {
+
+ return error('Invalid filter value ' + node[key] + ' for key ' + key);
+ }
+ }
+ else if (key === '$default') {
+ found.default = true;
+ var err = internals.validate(node.$default, path + '/$default');
+ if (err) {
+ return err;
+ }
+ }
+ else {
+ return error('Unknown $ directive ' + key);
+ }
+ }
+ else {
+ found.key = true;
+ if (!key.match(/\w+/)) {
+ return error('Invalid key string ' + key);
+ }
+
+ var value = node[key];
+ var err = internals.validate(value, path + '/' + key);
+ if (err) {
+ return err;
+ }
+ }
+ }
+
+ // Invalid directive combination
+
+ if (found.default && !found.filter) {
+ return error('Default value without a filter');
+ }
+
+ if (found.filter && !found.default && !found.key) {
+ return error('Filter without an values');
+ }
+
+ if (found.filter && found.default && !found.key) {
+ return error('Filter with only a default');
+ }
+
+ // Valid node
+
+ return null;
+};
+
+
+// Return node or value if no filter, otherwise apply filters until node or value
+
+internals.filter = function (node, criteria) {
+
+ if (!node ||
+ typeof node !== 'object' || // Value
+ !node.$filter) { // Fork
+
+ return node || null; // Override undefined
+ }
+
+ // Filter
+
+ var filter = node.$filter;
+ var criterion = criteria[filter];
+
+ if (criterion) {
+ var sub = node[criterion];
+ if (!sub) {
+ return null;
+ }
+
+ return internals.filter(sub, criteria);
+ }
+ else if (node.$default) {
+ return internals.filter(node.$default, criteria);
+ }
+ else {
+ return null;
+ }
+};
+
+
+// Applies criteria on an entire tree
+
+internals.walk = function (node, criteria, depth) {
+
+ if (!node ||
+ typeof node !== 'object') {
+
+ return node || null; // Override undefined
+ }
+
+ if (depth === 0) {
+ return {};
+ }
+
+ var parent = {};
+
+ var keys = Object.keys(node);
+ for (var i = 0, il = keys.length; i < il; ++i) {
+ var key = keys[i];
+ var child = internals.filter(node[key], criteria);
+ var value = internals.walk(child, criteria, depth !== undefined ? depth - 1 : undefined);
+ if (value) {
+ parent[key] = value;
+ }
+ }
+
+ return parent;
+};
View
49 test/index.js 100644 → 100755
@@ -0,0 +1,49 @@
+// Load modules
+
+var Lab = require('lab');
+var Hapi = require('hapi');
+
+
+// Declare internals
+
+var internals = {};
+
+
+// Test shortcuts
+
+var expect = Lab.expect;
+var before = Lab.before;
+var after = Lab.after;
+var describe = Lab.experiment;
+var it = Lab.test;
+
+
+describe('Confidence', function () {
+
+ describe('Plugin', function () {
+
+ var server = new Hapi.Server(0);
+
+ before(function (done) {
+
+ server.pack.require('../', {}, function (err) {
+
+ expect(err).to.not.exist;
+ server.start(function () {
+
+ done();
+ });
+ });
+ });
+
+ //it('returns root', function (done) {
+
+ // server.inject('/experimental/dat/department', function (res) {
+
+ // expect(res.statusCode).to.equal(200);
+ // expect(res.result.children.length).to.equal(31);
+ // done();
+ // });
+ //});
+ });
+});
View
97 test/store.js
@@ -0,0 +1,97 @@
+// Load modules
+
+var Lab = require('lab');
+var Hapi = require('hapi');
+var Store = require('../lib/store');
+
+
+// Declare internals
+
+var internals = {};
+
+
+// Test shortcuts
+
+var expect = Lab.expect;
+var before = Lab.before;
+var after = Lab.after;
+var describe = Lab.experiment;
+var it = Lab.test;
+
+
+describe('Confidence', function () {
+
+ describe('Storage', function () {
+
+ var tree = {
+ // Fork
+ key1: 'abc', // Value
+ key2: {
+ // Filter
+ $filter: 'env',
+ production: {
+ // Fork
+ deeper: 'value' // Value
+ },
+ $default: {
+ // Filter
+ $filter: 'platform',
+ ios: 1, // Value
+ $default: 2 // Value
+ }
+ },
+ key3: {
+ // Fork
+ sub1: 123,
+ sub2: {
+ // Filter
+ $filter: 'xfactor',
+ yes: 6 // Value
+ }
+ }
+ };
+
+ var store = new Store();
+ store.load(tree);
+
+ var get = function (key, value, criteria, depth) {
+
+ it('gets value for ' + key + (criteria ? ' with criteria ' + JSON.stringify(criteria) : ''), function (done) {
+
+ var result = store.get(key, criteria || {}, depth);
+ expect(result).to.deep.equal(value);
+ done();
+ });
+ };
+
+ get('key', null);
+ get('/key1', 'abc');
+ get('/key2', 2);
+ get('/key2', 1, { platform: 'ios' });
+ get('/key2/deeper', 'value', { env: 'production' });
+ get('/key2/deeper', null, { env: 'qa' });
+ get('/key2/deeper', null);
+ get('/', { key1: 'abc', key2: 2, key3: { sub1: 123 } });
+ get('/', { key1: 'abc', key2: 2, key3: { sub1: 123, sub2: 6 } }, { xfactor: 'yes' });
+ get('/', { key1: 'abc', key2: 2, key3: { } }, null, 1);
+ get('/', { key1: 'abc', key2: 2, key3: { sub1: 123 } }, null, 2);
+
+ var read = function (key, value) {
+
+ it('reads value for ' + key, function (done) {
+
+ var result = store.read(key);
+ expect(result).to.deep.equal(value);
+ done();
+ });
+ };
+
+ read('key1', null);
+ read('/', tree);
+ read('/key1', 'abc');
+ read('/key2/production/deeper', 'value');
+ read('/key2/$default/$filter', 'platform');
+ read('/key3/sub2', { $filter: 'xfactor', yes: 6 });
+ read('/key4', null);
+ });
+});
Please sign in to comment.
Something went wrong with that request. Please try again.