<commit_before>/*
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of perseo-fe
 *
 * perseo-fe is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * perseo-fe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with perseo-fe.
 * If not, see http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[contacto@tid.es]
 */
'use strict';

var util = require('util'),
    rules = require('./rules'),
    myutils = require('../myutils'),
    eplTemplate = 'select *,"${name}" as ruleName' +
        ' from pattern [every ev=iotEvent(${conditions})]',
    operatorMap = {
        'GREATER_THAN': ' > ',
        'EQUAL_TO': ' = ',
        'MINOR_THAN': ' < ',
        'DIFFERENT_TO': ' != ',
        'GREATER_OR_EQUAL_THAN': ' >= ',
        'MINOR_OR_EQUAL_THAN': ' <= ',
        'MATCH': ' regexp '
    },
    errors = {};

function castVar(str, isNumber) {
    str = util.format('cast(`%s`?, String)', str);
    if (isNumber === true) {
        str = util.format('cast(%s, float)', str);
    }
    return str;
}

function adaptExp(str, isNumber) {
    if (str.indexOf('${') !== 0) {
        if (isNumber !== true) {
            str = util.format('%j', str);
        }
        return str;
    }
    //Remove starting "${" and trailing "}"
    str = str.slice(2, -1);
    //Change dot to double underscore
    str = str.replace(/\./g, '__');
    return castVar(str, isNumber);
}

function makeUserPropCondition(exprL, op, exprR, isNumber) {
    exprL = adaptExp(exprL, isNumber);
    exprR = adaptExp(exprR, isNumber);
    return util.format('(%s %s %s)', exprL, operatorMap[op], exprR);
}
function makeObservationCondition(exprL, op, exprR, isNumber) {
    exprL = castVar(exprL, isNumber);
    exprR = adaptExp(exprR, isNumber);
    return util.format('(%s %s %s)', exprL, operatorMap[op], exprR);
}
function plainParams(p) {
    var i, obj = {};
    for (i = 0; i < p.length; i += 1) {
        obj[p[i].name] = p[i].value;
    }
    return obj;
}
function transformAxnParam(str) {
    str = str.replace(/\$\{([A-Za-z0-9_.]*)\}/g, function(m, g1) {
        return util.format('${ev__%s}', g1.replace(/\./g, '__'));
    });
    return str;
}
function vr2rule(cardRule) {
    var r, i, card,
        conditions = [],
        action = {},
        noSignal, id, idRegexp, type,
        plain;
    try {
        r = {VR: cardRule, name: cardRule.name};
        for (i = 0; i < cardRule.cards.length; i += 1) {
            card = cardRule.cards[i];
            switch (card.type) {
            case 'SensorCard':
                switch (card.conditionList[0].scope) {
                case 'USER_PROP':
                    conditions.push(makeUserPropCondition(card.conditionList[0].userProp,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                        false));

                    break;
                case 'XPATH':
                    conditions.push(makeObservationCondition(card.conditionList[0].parameterName,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                            card.sensorData.dataType === 'Quantity'));
                    if (card.conditionList[0].parameterName === 'id') {
                        if (card.conditionList[0].operator === 'MATCH') {
                            idRegexp = card.conditionList[0].parameterValue;
                            //Just to check it is a valid regexp
                            new RegExp(idRegexp);
                        } else {
                            id = card.conditionList[0].parameterValue;
                        }
                    } else if (card.conditionList[0].parameterName === 'type') {
                        type = card.conditionList[0].parameterValue;
                    }
                    break;
                case 'OBSERVATION':
                    conditions.push(makeObservationCondition(card.sensorData.measureName,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                            card.sensorData.dataType === 'Quantity'));
                    break;
                case 'LAST_MEASURE':
                    if (isNaN(parseInt(card.timeData.interval, 10))) {
                        return new errors.InvalidVisualRule('invalid period time ' + card.timeData.interval);
                    }
                    noSignal = {
                        checkInterval: card.timeData.interval,
                        attribute: card.sensorData.measureName,
                        reportInterval: card.conditionList[0].parameterValue };
                    break;
                default:
                    return new errors.NotSupportedActionScope(card.conditionList[0].scope);
                }
                break;
            case 'ActionCard':
                switch (card.actionData.type) {
                case 'SendEmailAction':
                    plain = plainParams(card.actionData.userParams);
                    action.type = 'email';
                    action.template = transformAxnParam(plain['mail.message']);
                    action.parameters = {
                        to: plain['mail.to'],
                        from: plain['mail.from'],
                        subject: plain['mail.subject']
                    };
                    break;
                case 'SendSmsMibAction':
                    plain = plainParams(card.actionData.userParams);
                    action.type = 'sms';
                    action.template = transformAxnParam(plain['sms.message']);
                    action.parameters = {
                        to: plain['sms.to']
                    };
                    break;
                case 'updateAttribute':
                    action.type = 'update';
                    action.parameters = {
                        name: card.actionData.userParams.name,
                        value: card.actionData.userParams.value
                    };
                    break;
                default:
                    return new errors.NotSupportedActionType(card.actionData.type);
                }
                break;

            case 'TimeCard':
                if (card.configData) {
                    switch (card.configData.timeType) {
                    case 'timeInterval': //No signal detection
                        // This is different from old DCA rules. No-signal cards are now of type
                        // SensorCard with a timeData field. https://jirapdi.tid.es/browse/PIOTP-855
                        return new errors.NotSupportedTimeCard(card.configData.timeType);
                        //break; not necessary with return
                    case 'timeElapsed': // Minimal interval for actions
                        action.interval = card.timeData.interval;
                        break;
                    }
                }
                else {
                    return new errors.MissingConfigDataInTimeCard(JSON.stringify(card));
                }

            }
        }

        r.action = action;
        r.service = cardRule.service;
        r.tenant = cardRule.tenant;
        if (noSignal) {
            r.nosignal = noSignal;
            r.nosignal.id = id;
            r.nosignal.idRegexp = idRegexp;
            r.nosignal.type = type;
        } else {
            r.text = myutils.expandVar(eplTemplate, {
                name: r.name,
                conditions: conditions.join(' AND '),
                tenant: r.tenant,
                service: r.service
            });
        }
    } catch (ex) { // SHOULD BE ex instanceof TypeError. Do not do anything else inside try
        // Also it can be instance of SyntaxError: Invalid regular expression
        return new errors.InvalidVisualRule(ex.toString());
    }
    return r;
}

module.exports = {
    FindAll: function FindAll(tenant, service, callback) {
        rules.FindAll(tenant, service, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            var vrdata = [], i;
            for (i = 0; i < data.length; i += 1) {
                if (data[i].VR) {
                    vrdata.push(data[i].VR);
                }
            }
            return callback(err, vrdata);
        });
    },
    Find: function Find(rule, callback) {
        rules.Find(rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            data = data || {};
            data.VR = data.VR || {};
            return callback(err, data.VR);
        });
    },
    Save: function Save(vrrule, callback) {
        var rule = vr2rule(vrrule);
        if (rule instanceof Error) {
            myutils.logErrorIf(rule);
            return callback(rule, null);
        }
        rules.Save(rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            return callback(null, data[2].VR);
        });
    },
    Remove: function Remove(id, callback) {
        rules.Remove(id, callback);
    },
    Put: function(id, rule, callback) {
        rule = vr2rule(rule);
        if (rule instanceof Error) {
            myutils.logErrorIf(rule);
            return callback(rule, null);
        }
        rules.Put(id, rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            callback(err, data[0].VR);
        });
    }
};
/**
 * Constructors for possible errors from this module
 *
 * @type {Object}
 */
module.exports.errors = errors;
module.exports.vr2rule = vr2rule;

(function() {

    errors.InvalidVisualRule = function InvalidVisualRule(msg) {
        this.name = 'INVALID_VISUAL_RULE';
        this.message = 'invalid visual rule format ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedTimeCard = function NotSupportedTimeCard(msg) {
        this.message = 'TimeCard not supported ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedActionType = function NotSupportedActionType(msg) {
        this.name = 'ACTION_TYPE_NOT_SUPPORTED';
        this.message = 'ActionType not supported ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedActionScope = function NotSupportedActionScope(msg) {
        this.name = 'UNKNOWN_SCOPE_ACTION';
        this.message = 'Scope not supported ' + msg;
        this.httpCode = 400;
    };
    errors.MissingConfigDataInTimeCard = function MissingConfigDataInTimeCard(msg) {
        this.name = 'MISSING_CONGIDATA_TIMECARD';
        this.message = 'Missing config data in time card' + msg;
        this.httpCode = 400;
    };
    Object.keys(errors).forEach(function(element) {
        util.inherits(errors[element], Error);
    });
})();

<commit_msg>Fix issue 55. Take updateAction parameters from array<commit_after>/*
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of perseo-fe
 *
 * perseo-fe is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * perseo-fe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with perseo-fe.
 * If not, see http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[contacto@tid.es]
 */
'use strict';

var util = require('util'),
    rules = require('./rules'),
    myutils = require('../myutils'),
    eplTemplate = 'select *,"${name}" as ruleName' +
        ' from pattern [every ev=iotEvent(${conditions})]',
    operatorMap = {
        'GREATER_THAN': ' > ',
        'EQUAL_TO': ' = ',
        'MINOR_THAN': ' < ',
        'DIFFERENT_TO': ' != ',
        'GREATER_OR_EQUAL_THAN': ' >= ',
        'MINOR_OR_EQUAL_THAN': ' <= ',
        'MATCH': ' regexp '
    },
    errors = {};

function castVar(str, isNumber) {
    str = util.format('cast(`%s`?, String)', str);
    if (isNumber === true) {
        str = util.format('cast(%s, float)', str);
    }
    return str;
}

function adaptExp(str, isNumber) {
    if (str.indexOf('${') !== 0) {
        if (isNumber !== true) {
            str = util.format('%j', str);
        }
        return str;
    }
    //Remove starting "${" and trailing "}"
    str = str.slice(2, -1);
    //Change dot to double underscore
    str = str.replace(/\./g, '__');
    return castVar(str, isNumber);
}

function makeUserPropCondition(exprL, op, exprR, isNumber) {
    exprL = adaptExp(exprL, isNumber);
    exprR = adaptExp(exprR, isNumber);
    return util.format('(%s %s %s)', exprL, operatorMap[op], exprR);
}
function makeObservationCondition(exprL, op, exprR, isNumber) {
    exprL = castVar(exprL, isNumber);
    exprR = adaptExp(exprR, isNumber);
    return util.format('(%s %s %s)', exprL, operatorMap[op], exprR);
}
function plainParams(p) {
    var i, obj = {};
    for (i = 0; i < p.length; i += 1) {
        obj[p[i].name] = p[i].value;
    }
    return obj;
}
function transformAxnParam(str) {
    str = str.replace(/\$\{([A-Za-z0-9_.]*)\}/g, function(m, g1) {
        return util.format('${ev__%s}', g1.replace(/\./g, '__'));
    });
    return str;
}
function vr2rule(cardRule) {
    var r, i, card,
        conditions = [],
        action = {},
        noSignal, id, idRegexp, type,
        plain;
    try {
        r = {VR: cardRule, name: cardRule.name};
        for (i = 0; i < cardRule.cards.length; i += 1) {
            card = cardRule.cards[i];
            switch (card.type) {
            case 'SensorCard':
                switch (card.conditionList[0].scope) {
                case 'USER_PROP':
                    conditions.push(makeUserPropCondition(card.conditionList[0].userProp,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                        false));

                    break;
                case 'XPATH':
                    conditions.push(makeObservationCondition(card.conditionList[0].parameterName,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                            card.sensorData.dataType === 'Quantity'));
                    if (card.conditionList[0].parameterName === 'id') {
                        if (card.conditionList[0].operator === 'MATCH') {
                            idRegexp = card.conditionList[0].parameterValue;
                            //Just to check it is a valid regexp
                            new RegExp(idRegexp);
                        } else {
                            id = card.conditionList[0].parameterValue;
                        }
                    } else if (card.conditionList[0].parameterName === 'type') {
                        type = card.conditionList[0].parameterValue;
                    }
                    break;
                case 'OBSERVATION':
                    conditions.push(makeObservationCondition(card.sensorData.measureName,
                        card.conditionList[0].operator,
                        card.conditionList[0].parameterValue,
                            card.sensorData.dataType === 'Quantity'));
                    break;
                case 'LAST_MEASURE':
                    if (isNaN(parseInt(card.timeData.interval, 10))) {
                        return new errors.InvalidVisualRule('invalid period time ' + card.timeData.interval);
                    }
                    noSignal = {
                        checkInterval: card.timeData.interval,
                        attribute: card.sensorData.measureName,
                        reportInterval: card.conditionList[0].parameterValue };
                    break;
                default:
                    return new errors.NotSupportedActionScope(card.conditionList[0].scope);
                }
                break;
            case 'ActionCard':
                switch (card.actionData.type) {
                case 'SendEmailAction':
                    plain = plainParams(card.actionData.userParams);
                    action.type = 'email';
                    action.template = transformAxnParam(plain['mail.message']);
                    action.parameters = {
                        to: plain['mail.to'],
                        from: plain['mail.from'],
                        subject: plain['mail.subject']
                    };
                    break;
                case 'SendSmsMibAction':
                    plain = plainParams(card.actionData.userParams);
                    action.type = 'sms';
                    action.template = transformAxnParam(plain['sms.message']);
                    action.parameters = {
                        to: plain['sms.to']
                    };
                    break;
                case 'updateAttribute':
                    action.type = 'update';
                    action.parameters = {
                        name: card.actionData.userParams[0].name,
                        value: card.actionData.userParams[0].value
                    };
                    break;
                default:
                    return new errors.NotSupportedActionType(card.actionData.type);
                }
                break;

            case 'TimeCard':
                if (card.configData) {
                    switch (card.configData.timeType) {
                    case 'timeInterval': //No signal detection
                        // This is different from old DCA rules. No-signal cards are now of type
                        // SensorCard with a timeData field. https://jirapdi.tid.es/browse/PIOTP-855
                        return new errors.NotSupportedTimeCard(card.configData.timeType);
                        //break; not necessary with return
                    case 'timeElapsed': // Minimal interval for actions
                        action.interval = card.timeData.interval;
                        break;
                    }
                }
                else {
                    return new errors.MissingConfigDataInTimeCard(JSON.stringify(card));
                }

            }
        }

        r.action = action;
        r.service = cardRule.service;
        r.tenant = cardRule.tenant;
        if (noSignal) {
            r.nosignal = noSignal;
            r.nosignal.id = id;
            r.nosignal.idRegexp = idRegexp;
            r.nosignal.type = type;
        } else {
            r.text = myutils.expandVar(eplTemplate, {
                name: r.name,
                conditions: conditions.join(' AND '),
                tenant: r.tenant,
                service: r.service
            });
        }
    } catch (ex) { // SHOULD BE ex instanceof TypeError. Do not do anything else inside try
        // Also it can be instance of SyntaxError: Invalid regular expression
        return new errors.InvalidVisualRule(ex.toString());
    }
    return r;
}

module.exports = {
    FindAll: function FindAll(tenant, service, callback) {
        rules.FindAll(tenant, service, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            var vrdata = [], i;
            for (i = 0; i < data.length; i += 1) {
                if (data[i].VR) {
                    vrdata.push(data[i].VR);
                }
            }
            return callback(err, vrdata);
        });
    },
    Find: function Find(rule, callback) {
        rules.Find(rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            data = data || {};
            data.VR = data.VR || {};
            return callback(err, data.VR);
        });
    },
    Save: function Save(vrrule, callback) {
        var rule = vr2rule(vrrule);
        if (rule instanceof Error) {
            myutils.logErrorIf(rule);
            return callback(rule, null);
        }
        rules.Save(rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            return callback(null, data[2].VR);
        });
    },
    Remove: function Remove(id, callback) {
        rules.Remove(id, callback);
    },
    Put: function(id, rule, callback) {
        rule = vr2rule(rule);
        if (rule instanceof Error) {
            myutils.logErrorIf(rule);
            return callback(rule, null);
        }
        rules.Put(id, rule, function(err, data) {
            if (err) {
                return callback(err, null);
            }
            callback(err, data[0].VR);
        });
    }
};
/**
 * Constructors for possible errors from this module
 *
 * @type {Object}
 */
module.exports.errors = errors;
module.exports.vr2rule = vr2rule;

(function() {

    errors.InvalidVisualRule = function InvalidVisualRule(msg) {
        this.name = 'INVALID_VISUAL_RULE';
        this.message = 'invalid visual rule format ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedTimeCard = function NotSupportedTimeCard(msg) {
        this.message = 'TimeCard not supported ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedActionType = function NotSupportedActionType(msg) {
        this.name = 'ACTION_TYPE_NOT_SUPPORTED';
        this.message = 'ActionType not supported ' + msg;
        this.httpCode = 400;
    };
    errors.NotSupportedActionScope = function NotSupportedActionScope(msg) {
        this.name = 'UNKNOWN_SCOPE_ACTION';
        this.message = 'Scope not supported ' + msg;
        this.httpCode = 400;
    };
    errors.MissingConfigDataInTimeCard = function MissingConfigDataInTimeCard(msg) {
        this.name = 'MISSING_CONGIDATA_TIMECARD';
        this.message = 'Missing config data in time card' + msg;
        this.httpCode = 400;
    };
    Object.keys(errors).forEach(function(element) {
        util.inherits(errors[element], Error);
    });
})();

<|endoftext|>


<commit_before>var methods = {};

var validateOne = function(validatorFunction, fieldName) {
  // Get field value.
  var value = this.get(fieldName);

  try {
    // Run validator function which throws exception which we have to catch and
    // add error message to appropriate field.
    validatorFunction.call(this, fieldName, value);

    // If a field's validation didn't throw an exception then clear an error
    // message for the given field.
    this._errors.delete(fieldName);

    return true;
  } catch (e) {
    if (e instanceof ValidationError) {
      this._errors.set(fieldName, e.message);
    } else {
      throw e;
    }

    return false;
  }
};

methods.validate = function(fieldName) {
  // Run validation for current and parent classes. The "isValid" variable will
  // be set to true when all validators for all classes passed validation.
  var isValid = Astro.utils.everyClass(this.constructor, function(Class) {
    // Get list of all validators for given class.
    var validators = Class.getValidators();

    if (fieldName) {
      // Get validation function for given field name.
      var validatorFunction = validators[fieldName];

      // Validate field only if there is validation function.
      if (validatorFunction) {
        return validateOne.call(this, validatorFunction, fieldName);
      } else {
        return true;
      }
    } else {
      // Take validators and run each one.
      return _.every(validators, validateOne, this);
    }
  }, this);

  return isValid;
};

methods.getValidationError = function() {
  console.warn('getValidationError had been renamed to getError');
  return this.getError.apply(this, arguments);
};

methods.hasValidationError = function() {
  console.warn('hasValidationError had been renamed to hasError');
  return this.hasError.apply(this, arguments);
};

methods.hasValidationErrors = function() {
  console.warn('hasValidationErrors had been renamed to hasErrors');
  return this.hasErrors.apply(this, arguments);
};

methods.getError = function(fieldName) {
  return this._errors.get(fieldName);
};

methods.getErrors = function() {
  return this._errors.all();
};

methods.hasError = function(fieldName) {
  return this._errors.has(fieldName);
};

methods.hasErrors = function() {
  return this._errors.size;
};

var events = {};

events.tojsonvalue = function(e) {
  var self = this;

  Tracker.nonreactive(function() {
    e.data.errors = self._errors.all();
  });
};

events.fromjsonvalue = function(e) {
  this._errors.set(e.data.errors);
};

onInitModule = function() {
  _.extend(Astro.BaseClass.prototype, methods);

  Astro.on('tojsonvalue', events.tojsonvalue);
  Astro.on('fromjsonvalue', events.fromjsonvalue);
};
<commit_msg>Return true/false from hasErrors method<commit_after>var methods = {};

var validateOne = function(validatorFunction, fieldName) {
  // Get field value.
  var value = this.get(fieldName);

  try {
    // Run validator function which throws exception which we have to catch and
    // add error message to appropriate field.
    validatorFunction.call(this, fieldName, value);

    // If a field's validation didn't throw an exception then clear an error
    // message for the given field.
    this._errors.delete(fieldName);

    return true;
  } catch (e) {
    if (e instanceof ValidationError) {
      this._errors.set(fieldName, e.message);
    } else {
      throw e;
    }

    return false;
  }
};

methods.validate = function(fieldName) {
  // Run validation for current and parent classes. The "isValid" variable will
  // be set to true when all validators for all classes passed validation.
  var isValid = Astro.utils.everyClass(this.constructor, function(Class) {
    // Get list of all validators for given class.
    var validators = Class.getValidators();

    if (fieldName) {
      // Get validation function for given field name.
      var validatorFunction = validators[fieldName];

      // Validate field only if there is validation function.
      if (validatorFunction) {
        return validateOne.call(this, validatorFunction, fieldName);
      } else {
        return true;
      }
    } else {
      // Take validators and run each one.
      return _.every(validators, validateOne, this);
    }
  }, this);

  return isValid;
};

methods.getValidationError = function() {
  console.warn('getValidationError had been renamed to getError');
  return this.getError.apply(this, arguments);
};

methods.hasValidationError = function() {
  console.warn('hasValidationError had been renamed to hasError');
  return this.hasError.apply(this, arguments);
};

methods.hasValidationErrors = function() {
  console.warn('hasValidationErrors had been renamed to hasErrors');
  return this.hasErrors.apply(this, arguments);
};

methods.getError = function(fieldName) {
  return this._errors.get(fieldName);
};

methods.getErrors = function() {
  return this._errors.all();
};

methods.hasError = function(fieldName) {
  return this._errors.has(fieldName);
};

methods.hasErrors = function() {
  return this._errors.size > 0;
};

var events = {};

events.tojsonvalue = function(e) {
  var self = this;

  Tracker.nonreactive(function() {
    e.data.errors = self._errors.all();
  });
};

events.fromjsonvalue = function(e) {
  this._errors.set(e.data.errors);
};

onInitModule = function() {
  _.extend(Astro.BaseClass.prototype, methods);

  Astro.on('tojsonvalue', events.tojsonvalue);
  Astro.on('fromjsonvalue', events.fromjsonvalue);
};
<|endoftext|>


<commit_before>"use strict";

const co        = require('co');
const constants = require('../constants');
const rules     = require('../rules');
const parsers   = require('../streams/parsers');

module.exports = (server) => new PermanentProver(server);

function PermanentProver(server) {

  const logger = require('../logger')('permprover');
  const that = this;

  let onBlockCallback = null,
      blockchainChangedResolver = null,
      powPromise = null,
      promiseOfWaitingBetween2BlocksOfOurs = null,
      lastComputedBlock = null;

  // Promises triggering the prooving lopp
  let resolveContinuePromise = null;
  let continuePromise = new Promise((resolve) => resolveContinuePromise = resolve);

  this.isPoWWaiting = () => !powPromise;

  this.allowedToStart = () => resolveContinuePromise(true);

  this.loops = 0;

  /******************
   * Main proof loop
   *****************/
  co(function*() {
    while (yield continuePromise) {
      try {
        const waitingRaces = [];

        // By default, we do not make a new proof
        let doProof = false;

        try {
          const selfPubkey = server.keyPair.publicKey;
          const dal = server.dal;
          const conf = server.conf;
          if (!conf.participate) {
            throw 'This node is configured for not participating to compute blocks, but this message is showing up. Weird.';
          }
          if (!selfPubkey) {
            throw 'No self pubkey found.';
          }
          let block, current;
          const isMember = yield dal.isMember(selfPubkey);
          if (!isMember) {
            throw 'Local node is not a member. Waiting to be a member before computing a block.';
          }
          current = yield dal.getCurrentBlockOrNull();
          if (!current) {
            throw 'Waiting for a root block before computing new blocks';
          }
          const version = current ? current.version : constants.BLOCK_GENERATED_VERSION;
          const trial = yield server.getBcContext().getIssuerPersonalizedDifficulty(version, selfPubkey);
          if (trial > (current.powMin + constants.POW_MAXIMUM_ACCEPTABLE_HANDICAP)) {
            logger.debug('Trial = %s, powMin = %s, pubkey = %s', trial, current.powMin, selfPubkey.slice(0, 6));
            throw 'Too high difficulty: waiting for other members to write next block';
          }
          const lastIssuedByUs = current.issuer == selfPubkey;
          const pullingPromise = server.PeeringService.pullingPromise();
          if (pullingPromise && !pullingPromise.isFulfilled()) {
            logger.warn('Waiting for the end of pulling...');
            yield pullingPromise;
            logger.warn('Pulling done. Continue proof-of-work loop.');
          }
          if (lastIssuedByUs && !promiseOfWaitingBetween2BlocksOfOurs) {
            promiseOfWaitingBetween2BlocksOfOurs = new Promise((resolve) => setTimeout(resolve, conf.powDelay));
            logger.warn('Waiting ' + conf.powDelay + 'ms before starting to compute next block...');
          } else {
            // We have waited enough
            promiseOfWaitingBetween2BlocksOfOurs = null;
            // But under some conditions, we can make one
            doProof = true;
          }
        } catch (e) {
          logger.warn(e);
        }

        if (doProof) {

          /*******************
           * COMPUTING A BLOCK
           ******************/
          if (!onBlockCallback) {
            throw Error('No callback has been provided to handle newly found proofs');
          }

          yield Promise.race([

            // We still listen at eventual blockchain change
            co(function*() {
              // If the blockchain changes
              yield new Promise((resolve) => blockchainChangedResolver = resolve);
              // Then cancel the generation
              yield server.BlockchainService.prover.cancel();
            }),

            // The generation
            co(function*() {
              try {
                const block2 = yield server.BlockchainService.generateNext();
                const trial2 = yield server.getBcContext().getIssuerPersonalizedDifficulty(block2.version, server.keyPair.publicKey);
                lastComputedBlock = yield server.BlockchainService.makeNextBlock(block2, trial2);
                yield onBlockCallback(lastComputedBlock);
              } catch (e) {
                logger.warn('The proof-of-work generation was canceled: %s', (e && e.message) || e || 'unkonwn reason');
              }
            })
          ]);
        } else {

          /*******************
           * OR WAITING PHASE
           ******************/
          if (promiseOfWaitingBetween2BlocksOfOurs) {
            waitingRaces.push(promiseOfWaitingBetween2BlocksOfOurs);
          }

          let raceDone = false;

          yield Promise.race(waitingRaces.concat([

            // The blockchain has changed! We or someone else found a proof, we must make a gnu one
            new Promise((resolve) => blockchainChangedResolver = () => {
              logger.warn('Blockchain changed!');
              resolve();
            }),

            // Security: if nothing happens for a while, trigger the whole process again
            new Promise((resolve) => setTimeout(() => {
              if (!raceDone) {
                logger.warn('Security trigger: proof-of-work process seems stuck');
                resolve();
              }
            }, constants.POW_SECURITY_RETRY_DELAY))
          ]));

          raceDone = true;
        }
      } catch (e) {
        logger.warn(e);
      }

      that.loops++;
      // Informative variable
      logger.trace('PoW loops = %s', that.loops);
    }
  });

  this.blockchainChanged = (gottenBlock) => co(function*() {
    if (!gottenBlock || !lastComputedBlock || gottenBlock.hash !== lastComputedBlock.hash) {
      // Cancel any processing proof
      yield server.BlockchainService.prover.cancel(gottenBlock);
      // If we were waiting, stop it and process the continuous generation
      blockchainChangedResolver && blockchainChangedResolver();
    }
  });

  this.stopEveryting = () => co(function*() {
    // First: avoid continuing the main loop
    continuePromise = new Promise((resolve) => resolveContinuePromise = resolve);
    // Second: stop any started proof
    yield server.BlockchainService.prover.cancel();
    // If we were waiting, stop it and process the continuous generation
    blockchainChangedResolver && blockchainChangedResolver();
  });

  this.onBlockComputed = (callback) => onBlockCallback = callback;
}
<commit_msg>[fix] #687 Too high proof-of-work was being processed anyway<commit_after>"use strict";

const co        = require('co');
const constants = require('../constants');
const rules     = require('../rules');
const parsers   = require('../streams/parsers');

module.exports = (server) => new PermanentProver(server);

function PermanentProver(server) {

  const logger = require('../logger')('permprover');
  const that = this;

  let onBlockCallback = null,
      blockchainChangedResolver = null,
      powPromise = null,
      promiseOfWaitingBetween2BlocksOfOurs = null,
      lastComputedBlock = null;

  // Promises triggering the prooving lopp
  let resolveContinuePromise = null;
  let continuePromise = new Promise((resolve) => resolveContinuePromise = resolve);

  this.isPoWWaiting = () => !powPromise;

  this.allowedToStart = () => resolveContinuePromise(true);

  this.loops = 0;

  /******************
   * Main proof loop
   *****************/
  co(function*() {
    while (yield continuePromise) {
      try {
        const waitingRaces = [];

        // By default, we do not make a new proof
        let doProof = false;

        try {
          const selfPubkey = server.keyPair.publicKey;
          const dal = server.dal;
          const conf = server.conf;
          if (!conf.participate) {
            throw 'This node is configured for not participating to compute blocks, but this message is showing up. Weird.';
          }
          if (!selfPubkey) {
            throw 'No self pubkey found.';
          }
          let block, current;
          const isMember = yield dal.isMember(selfPubkey);
          if (!isMember) {
            throw 'Local node is not a member. Waiting to be a member before computing a block.';
          }
          current = yield dal.getCurrentBlockOrNull();
          if (!current) {
            throw 'Waiting for a root block before computing new blocks';
          }
          const version = current ? current.version : constants.BLOCK_GENERATED_VERSION;
          const trial = yield server.getBcContext().getIssuerPersonalizedDifficulty(version, selfPubkey);
          checkTrialIsNotTooHigh(trial, current, selfPubkey);
          const lastIssuedByUs = current.issuer == selfPubkey;
          const pullingPromise = server.PeeringService.pullingPromise();
          if (pullingPromise && !pullingPromise.isFulfilled()) {
            logger.warn('Waiting for the end of pulling...');
            yield pullingPromise;
            logger.warn('Pulling done. Continue proof-of-work loop.');
          }
          if (lastIssuedByUs && !promiseOfWaitingBetween2BlocksOfOurs) {
            promiseOfWaitingBetween2BlocksOfOurs = new Promise((resolve) => setTimeout(resolve, conf.powDelay));
            logger.warn('Waiting ' + conf.powDelay + 'ms before starting to compute next block...');
          } else {
            // We have waited enough
            promiseOfWaitingBetween2BlocksOfOurs = null;
            // But under some conditions, we can make one
            doProof = true;
          }
        } catch (e) {
          logger.warn(e);
        }

        if (doProof) {

          /*******************
           * COMPUTING A BLOCK
           ******************/
          if (!onBlockCallback) {
            throw Error('No callback has been provided to handle newly found proofs');
          }

          yield Promise.race([

            // We still listen at eventual blockchain change
            co(function*() {
              // If the blockchain changes
              yield new Promise((resolve) => blockchainChangedResolver = resolve);
              // Then cancel the generation
              yield server.BlockchainService.prover.cancel();
            }),

            // The generation
            co(function*() {
              try {
                const current = yield server.dal.getCurrentBlockOrNull();
                const selfPubkey = server.keyPair.publicKey;
                const block2 = yield server.BlockchainService.generateNext();
                const trial2 = yield server.getBcContext().getIssuerPersonalizedDifficulty(block2.version, selfPubkey);
                checkTrialIsNotTooHigh(trial2, current, selfPubkey);
                lastComputedBlock = yield server.BlockchainService.makeNextBlock(block2, trial2);
                yield onBlockCallback(lastComputedBlock);
              } catch (e) {
                logger.warn('The proof-of-work generation was canceled: %s', (e && e.message) || e || 'unkonwn reason');
              }
            })
          ]);
        } else {

          /*******************
           * OR WAITING PHASE
           ******************/
          if (promiseOfWaitingBetween2BlocksOfOurs) {
            waitingRaces.push(promiseOfWaitingBetween2BlocksOfOurs);
          }

          let raceDone = false;

          yield Promise.race(waitingRaces.concat([

            // The blockchain has changed! We or someone else found a proof, we must make a gnu one
            new Promise((resolve) => blockchainChangedResolver = () => {
              logger.warn('Blockchain changed!');
              resolve();
            }),

            // Security: if nothing happens for a while, trigger the whole process again
            new Promise((resolve) => setTimeout(() => {
              if (!raceDone) {
                logger.warn('Security trigger: proof-of-work process seems stuck');
                resolve();
              }
            }, constants.POW_SECURITY_RETRY_DELAY))
          ]));

          raceDone = true;
        }
      } catch (e) {
        logger.warn(e);
      }

      that.loops++;
      // Informative variable
      logger.trace('PoW loops = %s', that.loops);
    }
  });

  this.blockchainChanged = (gottenBlock) => co(function*() {
    if (!gottenBlock || !lastComputedBlock || gottenBlock.hash !== lastComputedBlock.hash) {
      // Cancel any processing proof
      yield server.BlockchainService.prover.cancel(gottenBlock);
      // If we were waiting, stop it and process the continuous generation
      blockchainChangedResolver && blockchainChangedResolver();
    }
  });

  this.stopEveryting = () => co(function*() {
    // First: avoid continuing the main loop
    continuePromise = new Promise((resolve) => resolveContinuePromise = resolve);
    // Second: stop any started proof
    yield server.BlockchainService.prover.cancel();
    // If we were waiting, stop it and process the continuous generation
    blockchainChangedResolver && blockchainChangedResolver();
  });

  this.onBlockComputed = (callback) => onBlockCallback = callback;

  function checkTrialIsNotTooHigh(trial, current, selfPubkey) {
    if (trial > (current.powMin + constants.POW_MAXIMUM_ACCEPTABLE_HANDICAP)) {
      logger.debug('Trial = %s, powMin = %s, pubkey = %s', trial, current.powMin, selfPubkey.slice(0, 6));
      throw 'Too high difficulty: waiting for other members to write next block';
    }
  }
}

<|endoftext|>


<commit_before>import {type, pack} from '../../src/value'
import JsContext from '../../src/js-context/JsContext'

import test from 'tape'

test('JsContext', t => {
  let c = new JsContext()

  t.ok(c instanceof JsContext)
  t.equal(c.constructor.name, 'JsContext')

  t.end()
})

test('JsContext.callCode with no inputs, no errors and no output', t => {
  let c = new JsContext()
  t.plan(2)

  c.callCode('let x = 3\n\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'assign')
  })

  c.callCode('// Multiple lines and comments\nlet x = {\na:1\n\n}\n\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'assign')
  })
})

test('JsContext.callCode with no inputs, no errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.callCode('return 42').then(result => {
    t.deepEqual(result, {errors: null, output: pack(42)}, 'just an evaluation')
  })
  c.callCode('let x = 3\nreturn x*3').then(result => {
    t.deepEqual(result, {errors: null, output: pack(9)}, 'assign and return')
  })
  c.callCode('let x = 3\nx*3\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'no return so no output')
  })
})

test('JsContext.callCode with inputs and outputs but no errors', t => {
  let c = new JsContext()
  t.plan(2)

  c.callCode('return a*6', {a: pack(7)}).then(result => {
    t.deepEqual(result, {errors: null, output: pack(42)})
  })
  c.callCode('return a*b[1]', {a: pack(17), b: pack([1, 2, 3])}).then(result => {
    t.deepEqual(result, {errors: null, output: pack(34)})
  })
})

test('JsContext.callCode output multiline', t => {
  let c = new JsContext()
  t.plan(1)

  c.callCode(`return {
    jermaine: 'Hiphopopotamus',
    brett: 'Rhymnoceros'
  }`, null, {pack: false}).then(result => {
    t.deepEqual(result, {errors: null, output: { brett: 'Rhymnoceros', jermaine: 'Hiphopopotamus' }})
  })
})

test('JsContext.callCode with errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.callCode('foo').then(result => {
    t.deepEqual(result, {errors: [{line: 1, column: 1, message: 'ReferenceError: foo is not defined'}], output: null})
  })
  c.callCode('1\n2\n foo\n4').then(result => {
    t.deepEqual(result, {errors: [{line: 3, column: 2, message: 'ReferenceError: foo is not defined'}], output: null})
  })
  c.callCode('<>').then(result => {
    t.deepEqual(result, {errors: [{line: 0, column: 0, message: 'SyntaxError: Unexpected token <'}], output: null})
  })
})

test('JsContext.runCode', t => {
  let c = new JsContext()
  t.plan(6)

  c.runCode('foo = "bar"')
  t.equal(foo, 'bar', 'can set global variable') // eslint-disable-line no-undef

  c.runCode('foo').then(result => {
    t.deepEqual(result, {errors: null, output: pack('bar')}, 'can get global variable')
  })
  c.runCode('foo + "t_simpson"').then(result => {
    t.deepEqual(result, {errors: null, output: pack('bart_simpson')}, 'can get global variable expression')
  })
  c.runCode('foo\n42\n"lisa"').then(result => {
    t.deepEqual(result, {errors: null, output: pack('lisa')}, 'last value is returned')
  })
  c.runCode('\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'nothing returned when empty')
  })
  c.runCode('let x = 5').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'nothing returned when last line is statement')
  })
})

test('JsContext.runCode with errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.runCode('foogazi').then(result => {
    t.deepEqual(result, { errors: [ { column: 1, line: 1, message: 'ReferenceError: foogazi is not defined' } ], output: null })
  })
  c.runCode('2*45\nfoogazi').then(result => {
    t.deepEqual(result, { errors: [ { column: 1, line: 2, message: 'ReferenceError: foogazi is not defined' } ], output: null })
  })
  c.runCode('<>').then(result => {
    t.deepEqual(result, { errors: [ { column: 0, line: 0, message: 'SyntaxError: Unexpected token <' } ], output: null })
  })
})

test('JsContext.codeDependencies', t => {
  let c = new JsContext()
  t.plan(3)

  c.codeDependencies('foo').then(result => t.deepEqual(result, ['foo']))
  c.codeDependencies('let foo\n foo').then(result => t.deepEqual(result, []))
  c.codeDependencies('let foo').then(result => t.deepEqual(result, []))
})

test('JsContext.hasFunction', t => {
  let c = new JsContext()

  t.ok(c.hasFunction('type'))
  t.notOk(c.hasFunction('this_is_not_a_registered_function'))
  t.end()
})

test('JsContext.callFunction without function name', t => {
  let c = new JsContext()
  t.plan(1)

  t.throws(() => {
    c.callFunction()
  })
})

test('JsContext.callFunction with no inputs', t => {
  let c = new JsContext()
  t.plan(1)

  c.callFunction('type').then(result => {
    t.deepEqual(result, {output: pack('unknown'), errors: null})
  })
})

test('JsContext.callFunction with inputs and output', t => {
  let c = new JsContext()
  t.plan(1)

  c.callFunction('type', [[undefined, pack(1)]]).then(result => {
    t.deepEqual(result, {output: pack('integer'), errors: null})
  })
})

test('JsContext.callFunction with named arguments', t => {
  let c = new JsContext()
  t.plan(7)

  // TODO this uses a stochatic function for testing! Use a deterministic function with
  // named parameters

  c.callFunction('random_uniform', [[undefined, 10]], {pack: false}).then(result => {
    t.equal(type(result.output), 'array')
    t.equal(result.output.length, 10)
  })

  c.callFunction('random_uniform', [['n', 10]], {pack: false}).then(result => {
    t.equal(type(result.output), 'array')
    t.equal(result.output.length, 10)
  })

  c.callFunction('random_uniform', [['min', 100],['n', 1]], {pack: false}).then(result => {
    t.equal(type(result.output), 'float')
    t.ok(result.output < 100)
  })

  c.callFunction('random_uniform', [['foo', 'bar'],['n', 10]], {pack: false})
    .then(() => {
      t.fail('should not resolve')
    })
    .catch(error => {
      t.equal(error.message, 'Invalid named argument "foo"; valid names are "n", "min", "max"')
    })

})

test('JsContext.callFunction with error', t => {
  let c = new JsContext()
  t.plan(1)
  c._functions['foo'] = () => {
    throw new Error('nope')
  }
  c.callFunction('foo').then(result => {
    t.deepEqual(result, {errors: [ { column: 0, line: 0, message: 'Error: nope' } ], output: null})
  })
})
<commit_msg>Change tests for new API<commit_after>import {type, pack} from '../../src/value'
import JsContext from '../../src/js-context/JsContext'

import test from 'tape'

test('JsContext', t => {
  let c = new JsContext()

  t.ok(c instanceof JsContext)
  t.equal(c.constructor.name, 'JsContext')

  t.end()
})

test('JsContext.callCode with no inputs, no errors and no output', t => {
  let c = new JsContext()
  t.plan(2)

  c.callCode('let x = 3\n\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'assign')
  })

  c.callCode('// Multiple lines and comments\nlet x = {\na:1\n\n}\n\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'assign')
  })
})

test('JsContext.callCode with no inputs, no errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.callCode('return 42').then(result => {
    t.deepEqual(result, {errors: null, output: pack(42)}, 'just an evaluation')
  })
  c.callCode('let x = 3\nreturn x*3').then(result => {
    t.deepEqual(result, {errors: null, output: pack(9)}, 'assign and return')
  })
  c.callCode('let x = 3\nx*3\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'no return so no output')
  })
})

test('JsContext.callCode with inputs and outputs but no errors', t => {
  let c = new JsContext()
  t.plan(2)

  c.callCode('return a*6', {a: pack(7)}).then(result => {
    t.deepEqual(result, {errors: null, output: pack(42)})
  })
  c.callCode('return a*b[1]', {a: pack(17), b: pack([1, 2, 3])}).then(result => {
    t.deepEqual(result, {errors: null, output: pack(34)})
  })
})

test('JsContext.callCode output multiline', t => {
  let c = new JsContext()
  t.plan(1)

  c.callCode(`return {
    jermaine: 'Hiphopopotamus',
    brett: 'Rhymnoceros'
  }`, null, {pack: false}).then(result => {
    t.deepEqual(result, {errors: null, output: { brett: 'Rhymnoceros', jermaine: 'Hiphopopotamus' }})
  })
})

test('JsContext.callCode with errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.callCode('foo').then(result => {
    t.deepEqual(result, {errors: [{line: 1, column: 1, message: 'ReferenceError: foo is not defined'}], output: null})
  })
  c.callCode('1\n2\n foo\n4').then(result => {
    t.deepEqual(result, {errors: [{line: 3, column: 2, message: 'ReferenceError: foo is not defined'}], output: null})
  })
  c.callCode('<>').then(result => {
    t.deepEqual(result, {errors: [{line: 0, column: 0, message: 'SyntaxError: Unexpected token <'}], output: null})
  })
})

test('JsContext.runCode', t => {
  let c = new JsContext()
  t.plan(6)

  c.runCode('foo = "bar"')
  t.equal(foo, 'bar', 'can set global variable') // eslint-disable-line no-undef

  c.runCode('foo').then(result => {
    t.deepEqual(result, {errors: null, output: pack('bar')}, 'can get global variable')
  })
  c.runCode('foo + "t_simpson"').then(result => {
    t.deepEqual(result, {errors: null, output: pack('bart_simpson')}, 'can get global variable expression')
  })
  c.runCode('foo\n42\n"lisa"').then(result => {
    t.deepEqual(result, {errors: null, output: pack('lisa')}, 'last value is returned')
  })
  c.runCode('\n').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'nothing returned when empty')
  })
  c.runCode('let x = 5').then(result => {
    t.deepEqual(result, {errors: null, output: null}, 'nothing returned when last line is statement')
  })
})

test('JsContext.runCode with errors', t => {
  let c = new JsContext()
  t.plan(3)

  c.runCode('foogazi').then(result => {
    t.deepEqual(result, { errors: [ { column: 1, line: 1, message: 'ReferenceError: foogazi is not defined' } ], output: null })
  })
  c.runCode('2*45\nfoogazi').then(result => {
    t.deepEqual(result, { errors: [ { column: 1, line: 2, message: 'ReferenceError: foogazi is not defined' } ], output: null })
  })
  c.runCode('<>').then(result => {
    t.deepEqual(result, { errors: [ { column: 0, line: 0, message: 'SyntaxError: Unexpected token <' } ], output: null })
  })
})

test('JsContext.codeDependencies', t => {
  let c = new JsContext()
  t.plan(3)

  c.codeDependencies('foo').then(result => t.deepEqual(result, ['foo']))
  c.codeDependencies('let foo\n foo').then(result => t.deepEqual(result, []))
  c.codeDependencies('let foo').then(result => t.deepEqual(result, []))
})

test('JsContext.hasFunction', t => {
  let c = new JsContext()

  t.ok(c.hasFunction('type'))
  t.notOk(c.hasFunction('this_is_not_a_registered_function'))
  t.end()
})

test('JsContext.callFunction without function name', t => {
  let c = new JsContext()
  t.plan(1)

  t.throws(() => {
    c.callFunction()
  })
})

test('JsContext.callFunction with no inputs', t => {
  let c = new JsContext()
  t.plan(1)

  c.callFunction('type').then(result => {
    t.deepEqual(result, {output: pack('unknown'), errors: null})
  })
})

test('JsContext.callFunction with inputs and output', t => {
  let c = new JsContext()
  t.plan(1)

  c.callFunction('type', [pack(1)]).then(result => {
    t.deepEqual(result, {output: pack('integer'), errors: null})
  })
})

test('JsContext.callFunction with named arguments', t => {
  let c = new JsContext()
  t.plan(7)

  // TODO this uses a stochatic function for testing! Use a deterministic function with
  // named parameters

  c.callFunction('random_uniform', [10], {}, {pack: false}).then(result => {
    t.equal(type(result.output), 'array')
    t.equal(result.output.length, 10)
  })

  c.callFunction('random_uniform', [], {n: 10}, {pack: false}).then(result => {
    t.equal(type(result.output), 'array')
    t.equal(result.output.length, 10)
  })

  c.callFunction('random_uniform', [], {min: 100, n: 1}, {pack: false}).then(result => {
    t.equal(type(result.output), 'float')
    t.ok(result.output < 100)
  })

  c.callFunction('random_uniform', [], {min: 100, foo: 1}, {pack: false})
    .then(() => {
      t.fail('should not resolve')
    })
    .catch(error => {
      t.equal(error.message, 'Invalid named argument "foo"; valid names are "n", "min", "max"')
    })

})

test('JsContext.callFunction with error', t => {
  let c = new JsContext()
  t.plan(1)
  c._functions['foo'] = () => {
    throw new Error('nope')
  }
  c.callFunction('foo').then(result => {
    t.deepEqual(result, {errors: [ { column: 0, line: 0, message: 'Error: nope' } ], output: null})
  })
})
<|endoftext|>


<commit_before>#!/usr/bin/env node --harmony
'use strict';

module.exports = MongooseReplPlus;

var fs = require('fs');
var util = require('util');
var path = require('path');

var _ = require('lodash');

var ReplPlus = require(__dirname + '/repl-plus');

var verbose = false;

function MongooseReplPlus(mongoose, _verbose) {
    var self = this;
    if (!(self instanceof MongooseReplPlus)) {
        return new MongooseReplPlus(mongoose, _verbose);
    }
    verbose = _verbose;
    ReplPlus.call(self);
    self.mongoose = mongoose || require('mongoose');
    self.connected = false;
    return self;
}

util.inherits(MongooseReplPlus, ReplPlus);

_.extend(MongooseReplPlus.prototype, {
    connect: function(uri, options, callback) {
        var self = this;
        var db = self.mongoose.connection;
        if (!self.connected) {
            self.mongoose.connect(uri || 'mongodb://localhost/test', options, callback);
            db.on('open', function() {
                self.connected = true;
            });
            db.on('error', function(err) {
                var msg = util.format('connection error: %s\n', err);
                var output = self.repl && self.repl.outputStream.write || console.log;
                output(msg);
                if (self.repl) { self.repl.displayPrompt(); }
            });
        }
        return self;
    },
    include: function(model_path) {
        var self = this;
        var mod;
        var resolved_path = path.resolve(model_path);
        try {
            mod = require(resolved_path);
            /**
             * support best practice method of exporting: function(conn)
             */
            if (typeof(mod) === 'function') {
                mod(self.mongoose);
            }
        } catch(e) {
            self.repl && self.repl.outputStream.write('unable to load model(s) from:' + model_path);
            throw e;
        }
        return self;
    },
    includeDir: function(models_dir) {
        var self = this;
        var resolved_path = path.resolve(models_dir);
        var files = fs.readdirSync(resolved_path);
        _.each(files, function(file) {
            if (file !== 'index.js' && file.substr(-3) === '.js') {
                self.include(resolved_path + '/' + file);
            }
        });
        return self;
    },
    onStart: function() {
        var self = this;
        ReplPlus.prototype.onStart.call(self);
        _.each(self.mongoose.models, function(v, k) {
            self.context.models[k] = v;
            if (verbose) {
                self.repl.outputStream.write('loaded model: ' + k + '\n');
            }
        });
        self.repl.on('exit', function() {
            self.mongoose.connection.close();
        });
        self.repl.displayPrompt();
        return self;
    },
    defaultOpts: function(opts) {
        var self = this;
        opts = _.extend({prompt: 'mongoose+> '}, opts);
        var defaults = ReplPlus.prototype.defaultOpts.call(self, opts);
        _.extend(defaults.context, {
            mongoose: self.mongoose,
            models: {},
        });
        return defaults;
    },
});

if (!module.parent) {
    var program = require('commander');
    program
        .version('0.1.0')
        .option('-i, --include [model]', 'Include model')
        .option('-d, --dir [model directory]', 'Include model directory')
        .parse(process.argv);
    var repl = new MongooseReplPlus();
    repl.connect(program.args[0]);
    if (program.include) {
        repl.include(program.include)
    }
    if (program.dir) {
        repl.includeDir(program.dir)
    }
    repl.start();
}
<commit_msg>Add commands to the shell to allow dynamic loading/refreshing of models.<commit_after>#!/usr/bin/env node --harmony
'use strict';

module.exports = MongooseReplPlus;

var fs = require('fs');
var util = require('util');
var path = require('path');

var _ = require('lodash');

var ReplPlus = require(__dirname + '/repl-plus');

function MongooseReplPlus(mongoose, _verbose) {
    var self = this;
    if (!(self instanceof MongooseReplPlus)) {
        return new MongooseReplPlus(mongoose, _verbose);
    }
    ReplPlus.call(self);
    self.mongoose = mongoose || require('mongoose');
    self.connected = false;
    return self;
}

util.inherits(MongooseReplPlus, ReplPlus);

_.extend(MongooseReplPlus.prototype, {
    connect: function(uri, options, callback) {
        var self = this;
        var db = self.mongoose.connection;
        if (!self.connected) {
            self.mongoose.connect(uri || 'mongodb://localhost/test', options, callback);
            db.on('open', function() {
                self.connected = true;
            });
            db.on('error', function(err) {
                var msg = util.format('connection error: %s\n', err);
                var output = self.repl && self.repl.outputStream.write || console.log;
                output(msg);
                if (self.repl) { self.repl.displayPrompt(); }
            });
        }
        return self;
    },
    include: function(model_path) {
        var self = this;
        var mod;
        if (model_path) {
            var resolved_path = path.resolve(model_path);
            try {
                mod = require(resolved_path);
                /**
                 * support best practice method of exporting: function(conn)
                 */
                if (typeof(mod) === 'function') {
                    mod(self.mongoose);
                }
            } catch(e) {
                self.repl && self.repl.outputStream.write('unable to load model(s) from:' + model_path);
                throw e;
            }
        }
        if (self.repl) { self.repl.displayPrompt(); }
        return self;
    },
    includeDir: function(models_dir) {
        var self = this;
        if (models_dir) {
            var resolved_path = path.resolve(models_dir);
            var files = fs.readdirSync(resolved_path);
            _.each(files, function(file) {
                if (file !== 'index.js' && file.substr(-3) === '.js') {
                    self.include(resolved_path + '/' + file);
                }
            });
        }
        if (self.repl) { self.repl.displayPrompt(); }
        return self;
    },
    refreshModels: function(verbose) {
        var self = this;
        _.each(self.mongoose.models, function(v, k) {
            self.context.models[k] = v;
            if (verbose) {
                self.repl.outputStream.write('loaded model: ' + k + '\n');
            }
        });
        if (self.repl) { self.repl.displayPrompt(); }
        return self;
    },
    onStart: function() {
        var self = this;
        ReplPlus.prototype.onStart.call(self);
        self.refreshModels();
        self.repl.on('exit', function() {
            self.mongoose.connection.close();
        });
        self.addCommand('model', self.include.bind(self), 'include a model file');
        self.addCommand('modelD', self.includeDir.bind(self), 'include a directory of models');
        self.addCommand('models', self.refreshModels.bind(self), 'refresh models references');
        self.repl.displayPrompt();
        return self;
    },
    defaultOpts: function(opts) {
        var self = this;
        opts = _.extend({prompt: 'mongoose+> '}, opts);
        var defaults = ReplPlus.prototype.defaultOpts.call(self, opts);
        _.extend(defaults.context, {
            mongoose: self.mongoose,
            models: {},
        });
        return defaults;
    },
});

if (!module.parent) {
    var program = require('commander');
    program
        .version('0.1.0')
        .option('-i, --include [model]', 'Include model')
        .option('-d, --dir [model directory]', 'Include model directory')
        .parse(process.argv);
    var repl = new MongooseReplPlus();
    repl.connect(program.args[0]);
    if (program.include) {
        repl.include(program.include)
    }
    if (program.dir) {
        repl.includeDir(program.dir)
    }
    repl.start();
}
<|endoftext|>


<commit_before>import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { succeedAlways, succeedSometimes, failSometimes } from '../actions';

const noop = () => {};

class MakeRequests extends React.Component {

  buildHandler = (handler) => {
    const { successCallback, errorCallback } = this.props;
    return handler()
      .then(successCallback || noop)
      .catch(errorCallback || noop);
  }

  onSucceedAlways = () => this.buildHandler(this.props.onSucceedAlways)

  onSucceedSometimes = () => this.buildHandler(this.props.onSucceedSometimes)

  onFailSometimes = () => this.buildHandler(this.props.onFailSometimes)

  render() {
    return (

        <button onClick={this.onSucceedAlways}>Succeed Always</button>
        <button onClick={this.onSucceedSometimes}>Succeed Sometimes</button>
        <button onClick={this.onFailSometimes}>Fail Sometimes</button>

    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      onSucceedAlways: succeedAlways,
      onSucceedSometimes: succeedSometimes,
      onFailSometimes: failSometimes
    },
    dispatch
  );
}

const ConnectedComponent = connect(null, mapDispatchToProps)(MakeRequests);

export { MakeRequests as RawComponent };
export default ConnectedComponent;
<commit_msg>fix: no promises example<commit_after>import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { succeedAlways, succeedSometimes, failSometimes } from '../actions';

class MakeRequests extends React.Component {

  buildHandler = (handler) => {
    const { successCallback, errorCallback } = this.props;
    const result = handler();
    if (!result.then) {
      if (successCallback || errorCallback) alert('Offline config returnPromises is false!');
      return result;
    }
    return result.then(successCallback).catch(errorCallback);
  }

  onSucceedAlways = () => this.buildHandler(this.props.onSucceedAlways)

  onSucceedSometimes = () => this.buildHandler(this.props.onSucceedSometimes)

  onFailSometimes = () => this.buildHandler(this.props.onFailSometimes)

  render() {
    return (

        <button onClick={this.onSucceedAlways}>Succeed Always</button>
        <button onClick={this.onSucceedSometimes}>Succeed Sometimes</button>
        <button onClick={this.onFailSometimes}>Fail Sometimes</button>

    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      onSucceedAlways: succeedAlways,
      onSucceedSometimes: succeedSometimes,
      onFailSometimes: failSometimes
    },
    dispatch
  );
}

const ConnectedComponent = connect(null, mapDispatchToProps)(MakeRequests);

export { MakeRequests as RawComponent };
export default ConnectedComponent;
<|endoftext|>


<commit_before>/*
 * redis.js: Redis storage engine for nconf configuration(s)
 *
 * (C) 2011, Charlie Robbins
 *
 */

var async = require('async'),
    redis = require('redis'),
    nconf = require('nconf'),
    Memory = require('./memory').Memory;

//
// ### function Redis (options)
// #### @options {Object} Options for this instance
// Constructor function for the Redis nconf store which maintains
// a nested Redis key structure based on key delimiters `:`.
//
// e.g.
//     my:nested:key, 'value'
//       namespace:keys        ==> ['my']
//       namespace:nested:keys ==> ['key']
//       namespace:nested:key  ==> 'value'
//
var Redis = exports.Redis = function (options) {
  options        = options || {};
  this.type      = 'redis';
  this.namespace = options.namespace || 'nconf';
  this.host      = options.host || 'localhost';
  this.port      = options.port || 6379;
  this.ttl       = options.ttl  || 60 * 60 * 1000;
  this.cache     = new Memory();
  this.redis     = redis.createClient(options.port, options.host);
};

//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Redis.prototype.get = function (key, callback) {
  var self    = this,
      result  = {},
      now     = Date.now(),
      mtime   = this.cache.mtimes[key],
      fullKey = nconf.key(this.namespace, key);

  //
  // If the key exists in the cache and the ttl is less than
  // the value set for this instance, return from the cache.
  //
  if (mtime && now - mtime < this.ttl) {
    return callback(null, this.cache.get(key));
  }

  //
  // Get the set of all children keys for the `key` supplied. If the value
  // to be returned is an Object, this list will not be empty.
  //
  this.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
    function addValue (source, next) {
      self.get(nconf.key(key, source), function (err, value) {
        if (err) {
          return next(err);
        }

        result[source] = value;
        next();
      });
    }

    if (keys && keys.length > 0) {
      //
      // If the value to be retrieved is an Object, recursively attempt
      // to get the value from redis. Here we use a recursive call to `this.get`
      // to support nested Object keys.
      //
      async.forEach(keys, addValue, function (err) {
        if (err) {
          return callback(err);
        }

        self.cache.set(key, result);
        callback(null, result);
      })
    }
    else {
      //
      // If there are no keys, then the value to be retrieved is a literal
      // and we can simply return the value from redis directly.
      //
      self.redis.get(fullKey, function (err, value) {
        if (err) {
          return callback(err);
        }

        result = JSON.parse(value);
        if (result) {
          self.cache.set(key, result);
        }

        callback(null, result);
      });
    }
  });
};

//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Redis.prototype.set = function (key, value, callback) {
  var self = this,
      path = nconf.path(key);

  function addKey (partial, next) {
    var index  = path.indexOf(partial),
        base   = [self.namespace].concat(path.slice(0, index)),
        parent = nconf.key.apply(null, base.concat(['keys']));

    self.redis.sadd(parent, partial, next);
  };

  //
  // Iterate over the entire key path and add each key to the
  // parent key-set if it doesn't exist already.
  //
  async.forEach(path, addKey, function (err) {
    if (err) {
      return callback(err);
    }

    var fullKey = nconf.key(self.namespace, key);

    if (!Array.isArray(value) && typeof value === 'object') {
      //
      // If the value is an `Object` (and not an `Array`) then
      // nest into the value and set the child keys appropriately.
      // This is done for efficient lookup when setting Object keys.
      // (i.e. If you set and Object then wish to later retrieve only a
      // member of that Object, the entire Object need not be retrieved).
      //
      self._setObject(fullKey, value, callback);
    }
    else {
      //
      // If the value is a simple literal (or an `Array`) then JSON
      // stringify it and put it into Redis.
      //
      value = JSON.stringify(value);
      self.cache.set(key, value);
      self.redis.set(fullKey, value, callback);
    }
  });
};

//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Redis.prototype.clear = function (key, callback) {
  var self    = this,
      result  = {},
      path    = [this.namespace].concat(nconf.path(key)),
      last    = path.pop(),
      fullKey = nconf.key(this.namespace, key);

  //
  // Clear the key from the cache for this instance
  //
  this.cache.clear(key);

  //
  // Remove the `key` from the parent set of keys.
  //
  this.redis.srem(nconf.key.apply(null, path.concat(['keys'])), last, function (err) {
    //
    // Remove the value from redis by iterating over the set of keys (if any)
    // and deleting each value. If no keys, then just delete the simple literal.
    //
    self.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
      function removeValue (child, next) {
        //
        // Recursively call `self.clear` here to ensure we remove any
        // nested Objects completely from this instance.
        //
        self.clear(nconf.key(key, child), next);
      }

      if (keys && keys.length > 0) {
        //
        // If there are child keys then iterate over them,
        // removing each child along the way.
        //
        async.forEach(keys, removeValue, callback);
      }
      else {
        //
        // Otherwise if this is just a simple literal, then
        // simply remove it from Redis directly.
        //
        self.redis.del(fullKey, callback);
      }
    });
  });
};

//
// ### function save (value, callback)
// #### @value {Object} Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Redis.prototype.save = function (value, callback) {
  if (Array.isArray(value) || typeof value !== 'object') {
    return callback(new Error('`value` to be saved must be an object.'));
  }

  var self = this,
      keys = Object.keys(value);

  //
  // Clear all existing keys associated with this instance.
  //
  this.reset(function (err) {
    if (err) {
      return callback(err);
    }

    //
    // Iterate over the keys in the new value, setting each of them.
    //
    async.forEach(keys, function (key, next) {
      self.set(key, value[key], next);
    }, callback);
  });
};

//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Redis.prototype.load = function (callback) {
  var self   = this,
      result = {};

  this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, keys) {
    if (err) {
      return callback(err);
    }

    function addValue (key, next) {
      self.get(key, function (err, value) {
        if (err) {
          return next(err);
        }

        result[key] = value;
        next();
      });
    }

    async.forEach(keys, addValue, function (err) {
      return err ? callback(err) : callback(null, result);
    });
  });
};

//
// ### function reset (callback)
// #### @callback {function} Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Redis.prototype.reset = function (callback) {
  var self = this;

  //
  // Get the list of of top-level keys, then clear each of them
  //
  this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, existing) {
    if (err) {
      return callback(err);
    }

    async.forEach(existing, function (key, next) {
      self.clear(key, next);
    }, callback);
  });
};

//
// ### @private function _setObject (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Internal helper function for setting all keys of a nested object.
//
Redis.prototype._setObject = function (key, value, callback) {
  var self = this,
      keys = Object.keys(value);

  function addValue (child, next) {
    //
    // Add the child key to the parent key-set, then set the value.
    // Recursively call `_setObject` in the event of nested Object(s).
    //
    self.redis.sadd(nconf.key(key, 'keys'), child, function (err) {
      if (err) {
        return next(err);
      }

      var fullKey = nconf.key(key, child),
          childValue = value[child];

      if (!Array.isArray(childValue) && typeof childValue === 'object') {
        self._setObject(fullKey, childValue, next);
      }
      else {
        childValue = JSON.stringify(childValue);
        self.redis.set(fullKey, childValue, next);
      }
    });
  }

  //
  // Iterate over the keys of the Object and set the appropriate values.
  //
  async.forEach(keys, addValue, function (err) {
    return err ? callback(err) : callback();
  });
};<commit_msg>[api] Add support for Redis auth and optional callbacks.<commit_after>/*
 * redis.js: Redis storage engine for nconf configuration(s)
 *
 * (C) 2011, Charlie Robbins
 *
 */

var async = require('async'),
    redis = require('redis'),
    nconf = require('nconf'),
    Memory = require('./memory').Memory;

//
// ### function Redis (options)
// #### @options {Object} Options for this instance
// Constructor function for the Redis nconf store which maintains
// a nested Redis key structure based on key delimiters `:`.
//
// e.g.
//     my:nested:key, 'value'
//       namespace:keys        ==> ['my']
//       namespace:nested:keys ==> ['key']
//       namespace:nested:key  ==> 'value'
//
var Redis = exports.Redis = function (options) {
  options        = options || {};
  this.type      = 'redis';
  this.namespace = options.namespace || 'nconf';
  this.host      = options.host || 'localhost';
  this.port      = options.port || 6379;
  this.ttl       = options.ttl  || 60 * 60 * 1000;
  this.cache     = new Memory();
  this.redis     = redis.createClient(options.port, options.host);

  if (options.auth) {
    this.redis.auth(options.auth);
  }
};

//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Redis.prototype.get = function (key, callback) {
  var self    = this,
      result  = {},
      now     = Date.now(),
      mtime   = this.cache.mtimes[key],
      fullKey = nconf.key(this.namespace, key);

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  //
  // If the key exists in the cache and the ttl is less than
  // the value set for this instance, return from the cache.
  //
  if (mtime && now - mtime < this.ttl) {
    return callback(null, this.cache.get(key));
  }

  //
  // Get the set of all children keys for the `key` supplied. If the value
  // to be returned is an Object, this list will not be empty.
  //
  this.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
    function addValue (source, next) {
      self.get(nconf.key(key, source), function (err, value) {
        if (err) {
          return next(err);
        }

        result[source] = value;
        next();
      });
    }

    if (keys && keys.length > 0) {
      //
      // If the value to be retrieved is an Object, recursively attempt
      // to get the value from redis. Here we use a recursive call to `this.get`
      // to support nested Object keys.
      //
      async.forEach(keys, addValue, function (err) {
        if (err) {
          return callback(err);
        }

        self.cache.set(key, result);
        callback(null, result);
      })
    }
    else {
      //
      // If there are no keys, then the value to be retrieved is a literal
      // and we can simply return the value from redis directly.
      //
      self.redis.get(fullKey, function (err, value) {
        if (err) {
          return callback(err);
        }

        result = JSON.parse(value);
        if (result) {
          self.cache.set(key, result);
        }

        callback(null, result);
      });
    }
  });
};

//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Redis.prototype.set = function (key, value, callback) {
  var self = this,
      path = nconf.path(key);

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  function addKey (partial, next) {
    var index  = path.indexOf(partial),
        base   = [self.namespace].concat(path.slice(0, index)),
        parent = nconf.key.apply(null, base.concat(['keys']));

    self.redis.sadd(parent, partial, next);
  };

  //
  // Iterate over the entire key path and add each key to the
  // parent key-set if it doesn't exist already.
  //
  async.forEach(path, addKey, function (err) {
    if (err) {
      return callback(err);
    }

    var fullKey = nconf.key(self.namespace, key);

    if (!Array.isArray(value) && typeof value === 'object') {
      //
      // If the value is an `Object` (and not an `Array`) then
      // nest into the value and set the child keys appropriately.
      // This is done for efficient lookup when setting Object keys.
      // (i.e. If you set and Object then wish to later retrieve only a
      // member of that Object, the entire Object need not be retrieved).
      //
      self._setObject(fullKey, value, callback);
    }
    else {
      //
      // If the value is a simple literal (or an `Array`) then JSON
      // stringify it and put it into Redis.
      //
      value = JSON.stringify(value);
      self.cache.set(key, value);
      self.redis.set(fullKey, value, callback);
    }
  });
};

//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Redis.prototype.clear = function (key, callback) {
  var self    = this,
      result  = {},
      path    = [this.namespace].concat(nconf.path(key)),
      last    = path.pop(),
      fullKey = nconf.key(this.namespace, key);

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  //
  // Clear the key from the cache for this instance
  //
  this.cache.clear(key);

  //
  // Remove the `key` from the parent set of keys.
  //
  this.redis.srem(nconf.key.apply(null, path.concat(['keys'])), last, function (err) {
    //
    // Remove the value from redis by iterating over the set of keys (if any)
    // and deleting each value. If no keys, then just delete the simple literal.
    //
    self.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
      function removeValue (child, next) {
        //
        // Recursively call `self.clear` here to ensure we remove any
        // nested Objects completely from this instance.
        //
        self.clear(nconf.key(key, child), next);
      }

      if (keys && keys.length > 0) {
        //
        // If there are child keys then iterate over them,
        // removing each child along the way.
        //
        async.forEach(keys, removeValue, callback);
      }
      else {
        //
        // Otherwise if this is just a simple literal, then
        // simply remove it from Redis directly.
        //
        self.redis.del(fullKey, callback);
      }
    });
  });
};

//
// ### function save (value, callback)
// #### @value {Object} Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Redis.prototype.save = function (value, callback) {
  if (Array.isArray(value) || typeof value !== 'object') {
    return callback(new Error('`value` to be saved must be an object.'));
  }

  var self = this,
      keys = Object.keys(value);

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  //
  // Clear all existing keys associated with this instance.
  //
  this.reset(function (err) {
    if (err) {
      return callback(err);
    }

    //
    // Iterate over the keys in the new value, setting each of them.
    //
    async.forEach(keys, function (key, next) {
      self.set(key, value[key], next);
    }, callback);
  });
};

//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Redis.prototype.load = function (callback) {
  var self   = this,
      result = {};

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, keys) {
    if (err) {
      return callback(err);
    }

    function addValue (key, next) {
      self.get(key, function (err, value) {
        if (err) {
          return next(err);
        }

        result[key] = value;
        next();
      });
    }

    async.forEach(keys, addValue, function (err) {
      return err ? callback(err) : callback(null, result);
    });
  });
};

//
// ### function reset (callback)
// #### @callback {function} Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Redis.prototype.reset = function (callback) {
  var self = this;

  // Set the callback if not provided for "fire and forget"
  callback = callback || function () { };

  //
  // Get the list of of top-level keys, then clear each of them
  //
  this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, existing) {
    if (err) {
      return callback(err);
    }

    async.forEach(existing, function (key, next) {
      self.clear(key, next);
    }, callback);
  });
};

//
// ### @private function _setObject (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Internal helper function for setting all keys of a nested object.
//
Redis.prototype._setObject = function (key, value, callback) {
  var self = this,
      keys = Object.keys(value);

  function addValue (child, next) {
    //
    // Add the child key to the parent key-set, then set the value.
    // Recursively call `_setObject` in the event of nested Object(s).
    //
    self.redis.sadd(nconf.key(key, 'keys'), child, function (err) {
      if (err) {
        return next(err);
      }

      var fullKey = nconf.key(key, child),
          childValue = value[child];

      if (!Array.isArray(childValue) && typeof childValue === 'object') {
        self._setObject(fullKey, childValue, next);
      }
      else {
        childValue = JSON.stringify(childValue);
        self.redis.set(fullKey, childValue, next);
      }
    });
  }

  //
  // Iterate over the keys of the Object and set the appropriate values.
  //
  async.forEach(keys, addValue, function (err) {
    return err ? callback(err) : callback();
  });
};<|endoftext|>