Skip to content

Instantly share code, notes, and snippets.

@d5
Created May 15, 2015 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d5/adb58ea8eddcdee6eb73 to your computer and use it in GitHub Desktop.
Save d5/adb58ea8eddcdee6eb73 to your computer and use it in GitHub Desktop.
'use strict';
const Etcd = require('node-etcd'),
P = require('bluebird'),
async = P.coroutine,
_ = require('lodash'),
assert = require('assert');
function keyToPath(key) {
return '/' + key.replace(/\./g, '/');
}
function pathToKey(path) {
return path.slice(1).replace(/\//g, '.');
}
function keyToRawPath(key) {
return 'v2/keys/' + key.replace(/\./g, '/');
}
function isValidKey(key) {
return _.isString(key) && /[\w\d\.]+/.test(key);
}
function translateNode(node) {
if(node.dir) {
if(node.nodes) {
const obj = {};
_.each(node.nodes, function(subNode) {
const subKey = pathToKey(subNode.key.slice(node.key.length));
obj[subKey] = translateNode(subNode);
});
return obj;
} else {
return {};
}
} else {
return JSON.parse(node.value);
}
}
function isValidValueElement(value) {
return _.isNull(value) ||
_.isString(value) ||
_.isBoolean(value) ||
(_.isNumber(value) && !_.isNaN(value)) ||
_.isArray(value);
}
function Options() {
const self = this;
self.defaultOptions = {};
self.hosts = _.map((process.env.ETCD_HOSTS || '127.0.0.1:5000').split(','), _.trim);
self.etcd = P.promisifyAll(new Etcd(self.hosts));
}
Options.prototype.get = async(function* (key) {
const self = this;
console.assert(isValidKey(key));
try {
const res = yield self.etcd.rawAsync('GET', keyToRawPath(key), null, { recursive: true });
return translateNode(res[0].node);
} catch(err) {
if(err.errorCode === 100) {
return undefined; // key not found
} else {
throw err;
}
}
});
// TODO: setting an object is not an atomic operation
Options.prototype.set = async(function* (key, value) {
const self = this;
console.assert(isValidKey(key));
if(_.isUndefined(value)) {
// setting undefined = delete
try {
yield self.etcd.rawAsync('DELETE', keyToRawPath(key), null, { recursive: true });
} catch(ignored) { }
} else if(_.isPlainObject(value)) {
// delete before setting to new object
try {
yield self.etcd.rawAsync('DELETE', keyToRawPath(key), null, { recursive: true });
} catch(ignored) { }
// drop attributes of undefined value
value = JSON.parse(JSON.stringify(value));
const keys = _.keys(value);
if(keys.length) {
yield P.each(keys, async(function *(k) {
yield self.set(key + '.' + k, value[k]);
}));
} else {
yield self.etcd.rawAsync('PUT', keyToRawPath(key), null, { dir: true });
}
} else {
console.assert(isValidValueElement(value));
yield self.etcd.rawAsync('PUT', keyToRawPath(key), JSON.stringify(value), {});
}
});
Options.prototype.del = async(function* (key) {
const self = this;
console.assert(isValidKey(key));
try {
yield self.etcd.rawAsync('DELETE', keyToRawPath(key), null, { recursive: true });
} catch(err) {
// key not found: ignored
if(err.errorCode !== 100) { throw err; }
}
});
describe('Options', function() {
const options = new Options();
function get(key, expected) {
it('get("' + key + '") == ' + JSON.stringify(expected), async(function* () {
if(_.isArray(expected) || _.isPlainObject(expected)) {
assert.deepEqual(yield options.get(key), expected);
} else {
assert.strictEqual(yield options.get(key), expected);
}
}));
}
function set(key, val) {
it('set("' + key + '", ' + JSON.stringify(val) + ')', async(function* () {
yield options.set(key, val);
if(_.isArray(val) || _.isPlainObject(val)) {
assert.deepEqual(yield options.get(key), JSON.parse(JSON.stringify(val)));
} else {
assert.strictEqual(yield options.get(key), val);
}
}));
}
function del(key) {
it('del("' + key + '")', async(function* () {
yield options.del(key);
assert.strictEqual(yield options.get(key), undefined);
}));
}
describe('simple', function() {
del('foo');
set('foo', undefined);
set('foo', null);
set('foo', '');
set('foo', 'string value');
set('foo', 5264);
set('foo', -52.64);
set('foo', false);
set('foo', true);
set('foo', []);
set('foo', [1,2,3]);
set('foo', [1,'two',true]);
del('foo');
});
describe('set object value', function() {
del('foo');
set('foo', {});
set('foo', { key1: null });
set('foo', { key1: null, key2: 5264 });
set('foo', { key1: null, key2: 5264, key3: false });
set('foo', { key1: null, key2: 5264, key3: false, key4: [1,2,3] });
set('foo', { key1: {} });
set('foo', { key1: { key2: {} } });
set('foo', { key1: { key2: null } });
set('foo', { key1: { key2: undefined } });
set('foo', { key1: { key2: 'string value' } });
set('foo', { key1: { key3: { key4: 'string value '} }, key2: 5264 });
set('foo', { key1: 5264 });
del('foo');
});
describe('dotted keys', function() {
del('foo');
set('foo.key1', 5264);
get('foo', { key1: 5264 });
set('foo.key2', [1,2,3]);
get('foo', { key1: 5264, key2: [1,2,3] });
del('foo.key2');
get('foo', { key1: 5264 });
set('foo.key3', [1,2,3]);
get('foo', { key1: 5264, key3: [1,2,3] });
set('foo.key3', undefined);
get('foo', { key1: 5264 });
set('foo.key2.key4', 'string value');
get('foo', { key1: 5264, key2: { key4: 'string value' } });
del('foo.key2');
get('foo', { key1: 5264 });
del('foo');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment