<commit_before>/***
Copyright 2015 Alexej Magura

This file is part of Bebopt (https://github.com/amagura/bebopt)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***/
'use strict';

var util      = require('util')
  , clone     = require('clone')
  , basename  = require('path').basename
  ;

function Bebopt(app) {
  this.app = app ||
    (process.argv.length === 1) ? 'node' : basename(process.argv[1]);
  this._options = [];
  this._parent = null;
  this._cooked = [];
  this._results = {};
  this._usage = undefined;
  this._help = [];
  this._opts = [];
  this._args = [];
  this._safeContext = [
    '_usage',
    'printHelp',
    '_help',
    '_cooked',
    '_eaten',
    '_log',
    'app'
  ];
}

Bebopt.prototype._beatError = function(listName, name) {
  if (listName === 'short') {
    if (name.length > 1) {
      err = 'Bebopt: option name too long: \'' + name + '\'';
      throw new Error(err);
    }
  }
  return false;
};

Bebopt.prototype._makeOpt = function(name) {
  var new_op = name.replace(/^.*?([:]*)$/, '$1')
    , new_name = name.replace(/^(.*?)[:]*$/, '$1')
    , op = null
    ;

  switch (new_op) {
    case '::':
      op = 'optarg';
      break;
    case ':':
      op = 'arg';
      break;
    default:
      op = 'flag';
      break;
  }
  return {
    op: op,
    name : new_name
  };
};

Bebopt.prototype._extend = function(context, name, cb) {
  context.prototype[name] = cb; // FIXME, should this be `this', or `context'?
  return this;
};

[ 'shortOption', 'longOption' ].forEach(function(funcName) {
  var list = funcName.replace(/Option/, '');
  return Bebopt.prototype._extend(Bebopt, funcName, function(_name, fn) {
    var self = this
      , _ref = self._makeOpt(_name)
      , name = _ref.name
      , op = _ref.op
      ;

    // make sure that the desired option-name is valid
    self._beatError(list, name);

    // if function isn't defined, then
    // we are working with a reference to a parent option
    if (fn === undefined) {
      self._parentError();
      self._parent.type = self._parent.type || op;
      // clone is necessary so that we can set things without them
      // carrying over to the parent option
      var child = clone(self._parent);

      // give the child a 'pointer' to its parent
      child.parent = self._parent;
      //child.parent.index = child.index;
      ++child.index;
      // set the name of `child' to the value of `name'
      child.name = name;
      child.list = list;
      child.arg = undefined; // this field is `ARG` in `--help=(<ARG>)'

      // give parent a pointer to its child
      self._parent.child = child;
      self._options.push(child);
    } else {
      self._options.push({
        cb: fn,
        type: op,
        name: name,
        list: list,
        child: null,
        parent: null,
        arg: undefined
      });
      var option = self._options.pop(); // NOTE makes `options.length` 1 less
      option.index = self._options.length;
      self._options.push(option);
    }
    self._parent = self._options.slice(-1)[0];
    return self;
  });
});

[ 'lO', 'sO' ].forEach(function(funcName) {
  var list = /^sO$/.test(funcName) ? 'short' : 'long';
  return Bebopt.prototype._extend(Bebopt, funcName, function(name, cb, help) {
    var self = this;
    if (typeof(cb) === 'string') {
      if (help instanceof Function) {
        return self[list + 'Option'](name, help).help(cb);
      } else {
        return self[list + 'Option'](name).help(cb);
      }
    } else if (cb instanceof Function) {
      if (typeof(help) === 'string') {
        return self[list + 'Option'](name, cb).help(help);
      } else {
        return self[list + 'Option'](name, cb);
      }
    }
  });
});

Bebopt.prototype.longBeat = clone(Bebopt.prototype.longOption);
Bebopt.prototype.shortBeat = clone(Bebopt.prototype.shortOption);

Bebopt.prototype._parentError = function() {
  if (this._parent === null || this._parent === undefined) {
    var err = 'Bebopt: null parent ref: cannot apply';
    throw new Error(err);
  }
};

Bebopt.prototype.help = function(text) {
  this._parentError();
  this._parent.usage = text;
  this._parent = null;
  return this;
};

Bebopt.prototype.usage = function(text) {
  this._usage = text;
  return this;
};

Bebopt.prototype._makeHelp = function() {
  var self = this;
  this._options.forEach(function(opt) {
    var dashes = (opt.list === 'short') ? '-' : '--'
      , usage = self._parentOrChildUsage(opt);
    if (opt.parent === null) {
      if (opt.child === null) {
        self._help.push('  ' + dashes + opt.name + usage);
      } else {
        if (dashes === '--')
          self._help.push('  -' + opt.child.name + ', ' + dashes + opt.name + usage);
        else
          self._help.push('  ' + dashes + opt.name + ', --' + opt.child.name + usage);
      }
    }
  });
};

Bebopt.prototype._parentOrChildUsage = function(opt) {
  var self = this;
  if (typeof(opt.usage) !== 'string') {
    if (opt.child === null) {
      if (opt.parent === null)
        return ''; // if both are null
      else
        return self._options[opt.parent.index].usage; // if only child is null
    } else {
      if (opt.parent === null)
        return self._options[opt.child.index].usage;
      else
        return self._options[opt.parent.index].usage;
    }
  } else {
    return opt.usage;
  }
};

Bebopt.prototype.printHelp = function(fn) {
  var self = this
    , fn = (fn === undefined ? console.error : fn)
    , usage = (self._usage === undefined ? 'Usage: ' + self.app.toString() : self._usage)
    ;

  fn(usage);
  self._help.forEach(function(txt) {
    fn(txt);
  });
};

// sep -> separate
Bebopt.prototype._sepOptArg = function(opt) {
  var arg, _opt;
  if (/=/.test(opt))
    arg = opt.replace(/.*?=(.*)/, '$1');
  else
    arg = undefined;
  _opt = opt.replace(/(.*)?=.*/, '$1');
  return {
    opt: _opt,
    arg: arg
  };
};

Bebopt.prototype._catchSpaceDelimArgs = function(opt, list) {
  var self = this
    , type = this._getOption(opt, list).type
    ;
  if (type !== 'flag' && opt.optarg === undefined) {
    self._args.forEach(function(nonOpt, ind) {
      if (nonOpt.index === (opt.index + 1)) {
        opt.optarg = nonOpt.arg;
        delete self._args[ind]
      }
    });
  }
  return opt;
};

Bebopt.prototype._catchInvalidOpt = function(opt, list) {
  var self = this
    , option = this._getOption(opt, list)
    ;
  if (list === 'short' && option === undefined) {
    console.error(app.toString() + ': invalid option -- \'' + opt.arg + '\'');
    process.exit(1);
  } else if (list === 'long' && option === undefined) {
    console.error(app.toString() + ': unrecognized option \'--' + opt.arg + '\'');
    process.exit(1);
  }
};

// threshold is the lowest value to be incremented
Bebopt.prototype._syncIndexes = function(list, threshold) {
  list.forEach(function(elem) {
    if (elem.index >= threshold)
      ++elem.index;
  });
  return list;
};

Bebopt.prototype._getOption = function(opt, listName) {
  var self = this;
  var option = self._options.filter(function(mem) {
    if (opt.arg === mem.name && listName === mem.list)
      return mem;
  });
  return (option.length === 0 ? undefined : option[0]);
};

Bebopt.prototype._setOptionArg = function(opt, listName) {
  var self = this
    , option = self._getOption(opt, listName)
    ;

  option.arg = opt.optarg;
  if (option.child !== null)
    self._options[option.child.index].arg = opt.optarg;
  else if (option.parent !== null)
    self._options[option.parent.index].arg = opt.optarg;
  return undefined;
};

Bebopt.prototype._splitCombinedShorts = function() {
  var self = this;
  self._opts.forEach(function(elem, ind) {
    var dashes = elem.arg.replace(/^(--?).*/, '$1').length; // number of dashes
    if (dashes === 1) {
      var options = elem.arg.replace(/^-(.*)/, '$1').split('');
      options = options.reverse();
      elem.arg = options.pop();

      while (options.length > 0) {
        new_elem = clone(elem);
        new_elem.arg = options.pop();
        if (self._args[new_elem.index] !== undefined) {
          self._syncIndexes(self._args, new_elem.index);
        }
        self._syncIndexes(self._opts, new_elem.index + 1);
        ++new_elem.index;
        self._opts.push(new_elem);
      }
    }
  });
};

Bebopt.prototype._takesArg = function(opt, listName) {
  var self = this;
  self._catchInvalidOpt(opt, listName);
  var type = self._getOption(opt, listName).type;
  if (type !== 'flag')
    return true;
  else
    return false;
};

Bebopt.prototype._log = function(xyz) {
  console.log(util.inspect(xyz, { colors: true, depth: null }));
};

Bebopt.prototype._resolveOpts = function() {
  var self = this;
  self._splitCombinedShorts();
  self._opts.forEach(function(elem) {
    var dashes = elem.arg.replace(/^(--?).*/, '$1').length; // number of dashes
    elem.arg = elem.arg.replace(/^--?(.*)/, '$1');
    [
      [ 'long', 2 ],
      [ 'short', 1 ]
    ].forEach(function(arr) {
      if (dashes === arr[1]) {
        if (self._takesArg(elem, arr[0])) {
          elem = self._catchSpaceDelimArgs(elem, arr[0])
        }
        self._bindOptToList(elem, arr[0]);
        var option = self._getOption(elem, arr[0]);
        self._cooked.push(option);
      }
    });
  });
};

Bebopt.prototype._bindOptToList = function(opt, listName) {
  var self = this
    , type = self._getOption(opt, listName).type
    ;

  if (type === 'arg') {
    if (opt.optarg === undefined) {
      var err = self.app.toString();
      err += (listName === 'short'
              ? 'option requires an argument -- \'' + opt.arg + '\''
              : 'option \'--' + opt.arg + '\' requires an argument');
      console.error(err);
      process.exit(1);
    } else {
      self._setOptionArg(opt, listName);
    }
  } else if (type === 'optarg') {
    self._setOptionArg(opt, listName);
  } else if (type === 'flag') {
    if (opt.optarg !== undefined) {
      var err = self.app.toString();
      err += (listName === 'short'
              ? 'option doesn\'t allow an argument -- \'' + opt.arg + '\''
              : 'option \'--' + opt.arg + '\' doesn\'t allow an argument');
      console.error(err);
      process.exit(1);
    } else {
      self._setOptionArg(opt, listName);
    }
  }
};

Bebopt.prototype._gather = function() {
  var self = this
    , optend = false
    , noOpt = false
    ;
  process.argv.slice(2).forEach(function(arg, ind, arr) {
    if (/^--$/.test(arg))
      optend = true;
    else if (/^-$/.test(arg))
      noOpt = true; // prevents `-' from creating an empty option object
      // FIXME, what if people want to read STDIN on `-'?
    else
      if (noOpt) // FIXME, should `!noOpt' be used instead?
        null; // do nothing
      if (optend || /^[^-]+/.test(arg)) { // if optend or no dashes are found
        // then we are working with an non-option argument
        self._args.push({
          arg: arg,
          index: ind
        });
      } else { // else, we are working with an option
        var new_opt = self._sepOptArg(arg);
        self._opts.push({
          arg: new_opt.opt,
          optarg: new_opt.arg,
          index: ind
        });
      }
  });
};

Bebopt.prototype._runCallbacks = function(context) {
  var self = this
    , arg = null
    ;
  this._cooked.forEach(function(option) {
    arg = option.cb.apply(context, [ option.arg ]);
    self._results[option.name] = arg;
    if (option.child !== null) // FIXME this probably will clobber stuff
      self._results[option.child.name] = arg;
    if (option.parent !== null) // FIXME this will probably clobber stuff
      self._results[option.parent.name] = arg;
  });
};

Bebopt.prototype._makeCleanContext = function() {
  var self = this
    , context = clone(self)
    ;
  Object.keys(context).forEach(function(key) {
    if (self._safeContext.indexOf(key) === -1)
      delete context[key];
  });
  return context;
};

Bebopt.prototype.parse = function() {
  this._makeHelp();
  this._gather();
  this._resolveOpts();
  var context = this._makeCleanContext();
  this._runCallbacks(context);
  return this._results;
};

module.exports = Bebopt;
<commit_msg>added an else<commit_after>/***
Copyright 2015 Alexej Magura

This file is part of Bebopt (https://github.com/amagura/bebopt)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***/
'use strict';

var util      = require('util')
  , clone     = require('clone')
  , basename  = require('path').basename
  ;

function Bebopt(app) {
  this.app = app ||
    (process.argv.length === 1) ? 'node' : basename(process.argv[1]);
  this._options = [];
  this._parent = null;
  this._cooked = [];
  this._results = {};
  this._usage = undefined;
  this._help = [];
  this._opts = [];
  this._args = [];
  this._safeContext = [
    '_usage',
    'printHelp',
    '_help',
    '_cooked',
    '_eaten',
    '_log',
    'app'
  ];
}

Bebopt.prototype._beatError = function(listName, name) {
  if (listName === 'short') {
    if (name.length > 1) {
      err = 'Bebopt: option name too long: \'' + name + '\'';
      throw new Error(err);
    }
  }
  return false;
};

Bebopt.prototype._makeOpt = function(name) {
  var new_op = name.replace(/^.*?([:]*)$/, '$1')
    , new_name = name.replace(/^(.*?)[:]*$/, '$1')
    , op = null
    ;

  switch (new_op) {
    case '::':
      op = 'optarg';
      break;
    case ':':
      op = 'arg';
      break;
    default:
      op = 'flag';
      break;
  }
  return {
    op: op,
    name : new_name
  };
};

Bebopt.prototype._extend = function(context, name, cb) {
  context.prototype[name] = cb; // FIXME, should this be `this', or `context'?
  return this;
};

[ 'shortOption', 'longOption' ].forEach(function(funcName) {
  var list = funcName.replace(/Option/, '');
  return Bebopt.prototype._extend(Bebopt, funcName, function(_name, fn) {
    var self = this
      , _ref = self._makeOpt(_name)
      , name = _ref.name
      , op = _ref.op
      ;

    // make sure that the desired option-name is valid
    self._beatError(list, name);

    // if function isn't defined, then
    // we are working with a reference to a parent option
    if (fn === undefined) {
      self._parentError();
      self._parent.type = self._parent.type || op;
      // clone is necessary so that we can set things without them
      // carrying over to the parent option
      var child = clone(self._parent);

      // give the child a 'pointer' to its parent
      child.parent = self._parent;
      //child.parent.index = child.index;
      ++child.index;
      // set the name of `child' to the value of `name'
      child.name = name;
      child.list = list;
      child.arg = undefined; // this field is `ARG` in `--help=(<ARG>)'

      // give parent a pointer to its child
      self._parent.child = child;
      self._options.push(child);
    } else {
      self._options.push({
        cb: fn,
        type: op,
        name: name,
        list: list,
        child: null,
        parent: null,
        arg: undefined
      });
      var option = self._options.pop(); // NOTE makes `options.length` 1 less
      option.index = self._options.length;
      self._options.push(option);
    }
    self._parent = self._options.slice(-1)[0];
    return self;
  });
});

[ 'lO', 'sO' ].forEach(function(funcName) {
  var list = /^sO$/.test(funcName) ? 'short' : 'long';
  return Bebopt.prototype._extend(Bebopt, funcName, function(name, cb, help) {
    var self = this;
    if (typeof(cb) === 'string') {
      if (help instanceof Function) {
        return self[list + 'Option'](name, help).help(cb);
      } else {
        return self[list + 'Option'](name).help(cb);
      }
    } else if (cb instanceof Function) {
      if (typeof(help) === 'string') {
        return self[list + 'Option'](name, cb).help(help);
      } else {
        return self[list + 'Option'](name, cb);
      }
    } else {
      return self[list + 'Option'](name, cb).help(help);
    }
  });
});

Bebopt.prototype.longBeat = clone(Bebopt.prototype.longOption);
Bebopt.prototype.shortBeat = clone(Bebopt.prototype.shortOption);

Bebopt.prototype._parentError = function() {
  if (this._parent === null || this._parent === undefined) {
    var err = 'Bebopt: null parent ref: cannot apply';
    throw new Error(err);
  }
};

Bebopt.prototype.help = function(text) {
  this._parentError();
  this._parent.usage = text;
  this._parent = null;
  return this;
};

Bebopt.prototype.usage = function(text) {
  this._usage = text;
  return this;
};

Bebopt.prototype._makeHelp = function() {
  var self = this;
  this._options.forEach(function(opt) {
    var dashes = (opt.list === 'short') ? '-' : '--'
      , usage = self._parentOrChildUsage(opt);
    if (opt.parent === null) {
      if (opt.child === null) {
        self._help.push('  ' + dashes + opt.name + usage);
      } else {
        if (dashes === '--')
          self._help.push('  -' + opt.child.name + ', ' + dashes + opt.name + usage);
        else
          self._help.push('  ' + dashes + opt.name + ', --' + opt.child.name + usage);
      }
    }
  });
};

Bebopt.prototype._parentOrChildUsage = function(opt) {
  var self = this;
  if (typeof(opt.usage) !== 'string') {
    if (opt.child === null) {
      if (opt.parent === null)
        return ''; // if both are null
      else
        return self._options[opt.parent.index].usage; // if only child is null
    } else {
      if (opt.parent === null)
        return self._options[opt.child.index].usage;
      else
        return self._options[opt.parent.index].usage;
    }
  } else {
    return opt.usage;
  }
};

Bebopt.prototype.printHelp = function(fn) {
  var self = this
    , fn = (fn === undefined ? console.error : fn)
    , usage = (self._usage === undefined ? 'Usage: ' + self.app.toString() : self._usage)
    ;

  fn(usage);
  self._help.forEach(function(txt) {
    fn(txt);
  });
};

// sep -> separate
Bebopt.prototype._sepOptArg = function(opt) {
  var arg, _opt;
  if (/=/.test(opt))
    arg = opt.replace(/.*?=(.*)/, '$1');
  else
    arg = undefined;
  _opt = opt.replace(/(.*)?=.*/, '$1');
  return {
    opt: _opt,
    arg: arg
  };
};

Bebopt.prototype._catchSpaceDelimArgs = function(opt, list) {
  var self = this
    , type = this._getOption(opt, list).type
    ;
  if (type !== 'flag' && opt.optarg === undefined) {
    self._args.forEach(function(nonOpt, ind) {
      if (nonOpt.index === (opt.index + 1)) {
        opt.optarg = nonOpt.arg;
        delete self._args[ind]
      }
    });
  }
  return opt;
};

Bebopt.prototype._catchInvalidOpt = function(opt, list) {
  var self = this
    , option = this._getOption(opt, list)
    ;
  if (list === 'short' && option === undefined) {
    console.error(app.toString() + ': invalid option -- \'' + opt.arg + '\'');
    process.exit(1);
  } else if (list === 'long' && option === undefined) {
    console.error(app.toString() + ': unrecognized option \'--' + opt.arg + '\'');
    process.exit(1);
  }
};

// threshold is the lowest value to be incremented
Bebopt.prototype._syncIndexes = function(list, threshold) {
  list.forEach(function(elem) {
    if (elem.index >= threshold)
      ++elem.index;
  });
  return list;
};

Bebopt.prototype._getOption = function(opt, listName) {
  var self = this;
  var option = self._options.filter(function(mem) {
    if (opt.arg === mem.name && listName === mem.list)
      return mem;
  });
  return (option.length === 0 ? undefined : option[0]);
};

Bebopt.prototype._setOptionArg = function(opt, listName) {
  var self = this
    , option = self._getOption(opt, listName)
    ;

  option.arg = opt.optarg;
  if (option.child !== null)
    self._options[option.child.index].arg = opt.optarg;
  else if (option.parent !== null)
    self._options[option.parent.index].arg = opt.optarg;
  return undefined;
};

Bebopt.prototype._splitCombinedShorts = function() {
  var self = this;
  self._opts.forEach(function(elem, ind) {
    var dashes = elem.arg.replace(/^(--?).*/, '$1').length; // number of dashes
    if (dashes === 1) {
      var options = elem.arg.replace(/^-(.*)/, '$1').split('');
      options = options.reverse();
      elem.arg = options.pop();

      while (options.length > 0) {
        new_elem = clone(elem);
        new_elem.arg = options.pop();
        if (self._args[new_elem.index] !== undefined) {
          self._syncIndexes(self._args, new_elem.index);
        }
        self._syncIndexes(self._opts, new_elem.index + 1);
        ++new_elem.index;
        self._opts.push(new_elem);
      }
    }
  });
};

Bebopt.prototype._takesArg = function(opt, listName) {
  var self = this;
  self._catchInvalidOpt(opt, listName);
  var type = self._getOption(opt, listName).type;
  if (type !== 'flag')
    return true;
  else
    return false;
};

Bebopt.prototype._log = function(xyz) {
  console.log(util.inspect(xyz, { colors: true, depth: null }));
};

Bebopt.prototype._resolveOpts = function() {
  var self = this;
  self._splitCombinedShorts();
  self._opts.forEach(function(elem) {
    var dashes = elem.arg.replace(/^(--?).*/, '$1').length; // number of dashes
    elem.arg = elem.arg.replace(/^--?(.*)/, '$1');
    [
      [ 'long', 2 ],
      [ 'short', 1 ]
    ].forEach(function(arr) {
      if (dashes === arr[1]) {
        if (self._takesArg(elem, arr[0])) {
          elem = self._catchSpaceDelimArgs(elem, arr[0])
        }
        self._bindOptToList(elem, arr[0]);
        var option = self._getOption(elem, arr[0]);
        self._cooked.push(option);
      }
    });
  });
};

Bebopt.prototype._bindOptToList = function(opt, listName) {
  var self = this
    , type = self._getOption(opt, listName).type
    ;

  if (type === 'arg') {
    if (opt.optarg === undefined) {
      var err = self.app.toString();
      err += (listName === 'short'
              ? 'option requires an argument -- \'' + opt.arg + '\''
              : 'option \'--' + opt.arg + '\' requires an argument');
      console.error(err);
      process.exit(1);
    } else {
      self._setOptionArg(opt, listName);
    }
  } else if (type === 'optarg') {
    self._setOptionArg(opt, listName);
  } else if (type === 'flag') {
    if (opt.optarg !== undefined) {
      var err = self.app.toString();
      err += (listName === 'short'
              ? 'option doesn\'t allow an argument -- \'' + opt.arg + '\''
              : 'option \'--' + opt.arg + '\' doesn\'t allow an argument');
      console.error(err);
      process.exit(1);
    } else {
      self._setOptionArg(opt, listName);
    }
  }
};

Bebopt.prototype._gather = function() {
  var self = this
    , optend = false
    , noOpt = false
    ;
  process.argv.slice(2).forEach(function(arg, ind, arr) {
    if (/^--$/.test(arg))
      optend = true;
    else if (/^-$/.test(arg))
      noOpt = true; // prevents `-' from creating an empty option object
      // FIXME, what if people want to read STDIN on `-'?
    else
      if (noOpt) // FIXME, should `!noOpt' be used instead?
        null; // do nothing
      if (optend || /^[^-]+/.test(arg)) { // if optend or no dashes are found
        // then we are working with an non-option argument
        self._args.push({
          arg: arg,
          index: ind
        });
      } else { // else, we are working with an option
        var new_opt = self._sepOptArg(arg);
        self._opts.push({
          arg: new_opt.opt,
          optarg: new_opt.arg,
          index: ind
        });
      }
  });
};

Bebopt.prototype._runCallbacks = function(context) {
  var self = this
    , arg = null
    ;
  this._cooked.forEach(function(option) {
    arg = option.cb.apply(context, [ option.arg ]);
    self._results[option.name] = arg;
    if (option.child !== null) // FIXME this probably will clobber stuff
      self._results[option.child.name] = arg;
    if (option.parent !== null) // FIXME this will probably clobber stuff
      self._results[option.parent.name] = arg;
  });
};

Bebopt.prototype._makeCleanContext = function() {
  var self = this
    , context = clone(self)
    ;
  Object.keys(context).forEach(function(key) {
    if (self._safeContext.indexOf(key) === -1)
      delete context[key];
  });
  return context;
};

Bebopt.prototype.parse = function() {
  this._makeHelp();
  this._gather();
  this._resolveOpts();
  var context = this._makeCleanContext();
  this._runCallbacks(context);
  return this._results;
};

module.exports = Bebopt;
<|endoftext|>


<commit_before>var Bittrex = require('node.bittrex.api');
var util = require('../core/util.js');
var _ = require('lodash');
var moment = require('moment');
var log = require('../core/log');
var apiKeyManager = require('../web/apiKeyManager.js');

// Helper methods
function joinCurrencies(currencyA, currencyB){
    return currencyA + '-' + currencyB;
}

var Trader = function(config) {
  _.bindAll(this);

  // not nice but works to use a web module here to grab the keys
  var keys = apiKeyManager._getApiKeyPair('bittrex');

  if(_.isObject(keys)) {
    config.key = keys.key;
    config.secret = keys.secret;
  } else {
    // no api key defined -> we need to set a dummy key, otherwise the Bittrex module will not work even for public requests
    config.key = 'sdfsdf32424dfdb';
    config.secret = 'sdfsdf32424dfdb';
  }

  if(_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.currency = config.currency;
    this.asset = config.asset;
  }

  this.name = 'Bittrex';
  this.balance;
  this.price;

  this.pair = [this.currency, this.asset].join('-');

  Bittrex.options({
    'apikey':  this.key,
    'apisecret': this.secret,
    'stream': false,
    'verbose': false,
    'cleartext': false
  });

  this.logAction('Init', 'New Trader', {currency: this.currency, asset: this.asset});

  this.bittrexApi = Bittrex;
}

// if the exchange errors we try the same call again after
// waiting 10 seconds
Trader.prototype.retry = function(method, args) {
  var wait = +moment.duration(10, 'seconds');
  log.debug(this.name, 'returned an error, retrying..');

  this.logAction('retry', 'returned an error, retrying..');

  var self = this;

  // make sure the callback (and any other fn)
  // is bound to Trader
  _.each(args, function(arg, i) {
    if(_.isFunction(arg))
      args[i] = _.bind(arg, self);
  });

  // run the failed method again with the same
  // arguments after wait
  setTimeout(
    function() { method.apply(self, args) },
    wait
  );
}

Trader.prototype.getPortfolio = function(callback) {
  var args = _.toArray(arguments);
  this.logAction('getPortfolio', 'called');

  var set = function(data, err) {
    if(err)
      return this.retry(this.getPortfolio, args);

    data = data.result;

    var assetEntry = _.find(data, function(i) { return i.Currency == this.asset}.bind(this));
    var currencyEntry = _.find(data, function(i) { return i.Currency == this.currency}.bind(this));

    if(_.isUndefined(assetEntry)) {
      assetEntry = {
        Available: 0.0,
        Currency: this.asset
      }
    }

    if(_.isUndefined(currencyEntry)) {
      currencyEntry = {
        Available: 0.0,
        Currency: this.currency
      }
    }

    var assetAmount = parseFloat( assetEntry.Available );
    var currencyAmount = parseFloat( currencyEntry.Available );

    if(
      !_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
      !_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
    ) {
      log.info('asset:', this.asset);
      log.info('currency:', this.currency);
      log.info('exchange data:', data);
      util.die('Gekko was unable to set the portfolio');
    }

    var portfolio = [
      { name: this.asset, amount: assetAmount },
      { name: this.currency, amount: currencyAmount }
    ];

    this.logAction('getPortfolio', 'result:', portfolio);

    callback(err, portfolio);
  }.bind(this);

  this.bittrexApi.getbalances(set);
}

Trader.prototype.getTicker = function(callback) {
  var args = _.toArray(arguments);

  this.logAction('getTicker', 'called');

  this.bittrexApi.getticker({market: this.pair}, function(data, err) {
    if(err)
      return this.retry(this.getTicker, args);

    var tick = data.result;

    this.logAction('getTicker', 'result', tick);

    callback(null, {
      bid: parseFloat(tick.Bid),
      ask: parseFloat(tick.Ask),
    })

  }.bind(this));
}

Trader.prototype.getFee = function(callback) {

  this.logAction('getFee', 'called');
  /*var set = function(data, err) {
    if(err || data.error)
      return callback(err || data.error);

    data = data.result;

    var assetEntry = _.find(data, function(c) { return c.Currency == this.asset}.bind(this));
    //var currencyEntry = _.find(data, function(c) { return c.Currency == this.currency}.bind(this));

    var fee = parseFloat(assetEntry.TxFee);

    callback(false, parseFloat(assetEntry.TxFee));
  }.bind(this);

  this.bittrexApi.getcurrencies(set); */

   callback(false, parseFloat(0.00025));
}

Trader.prototype.buy = function(amount, price, callback) {
  var args = _.toArray(arguments);

   this.logAction('buy', 'called');

  var set = function(result, err) {
    if(err || result.error) {
      log.error('unable to buy:', err, result);
      return this.retry(this.buy, args);
    }

    this.logAction('buy', 'result', result);

    callback(null, result.result.uuid);
  }.bind(this);

  this.bittrexApi.buylimit({market: this.pair, quantity: amount, rate: price}, set);
}

Trader.prototype.sell = function(amount, price, callback) {
  var args = _.toArray(arguments);

  this.logAction('sell', 'called');

  var set = function(result, err) {
    if(err || result.error) {
      log.error('unable to sell:', err, result);
      return this.retry(this.sell, args);
    }

   this.logAction('sell', 'result', result);

    callback(null, result.result.uuid);
  }.bind(this);

  this.bittrexApi.selllimit({market: this.pair, quantity: amount, rate: price}, set);
}

Trader.prototype.checkOrder = function(order, callback) {
  var check = function(result, err) {
    this.logAction('checkOrder', 'called');

    var stillThere = _.find(result.result, function(o) { return o.OrderUuid === order });

    this.logAction('checkOrder', 'result', stillThere);
    callback(err, !stillThere);
  }.bind(this);

  this.bittrexApi.getopenorders({market: this.pair}, check);
}

Trader.prototype.getOrder = function(order, callback) {

  var get = function(result, err) {

    this.logAction('getOrder', 'called');

    if(err)
      return callback(err);

    var price = 0;
    var amount = 0;
    var date = moment(0);

    if(!result.success)
      return callback(null, {price, amount, date});

    var resultOrder = result.result;

    price = resultOrder.Price;
    amount = resultOrder.Quantity;

    if(resultOrder.IsOpen) {
         date = moment(resultOrder.Opened);
    } else {
       date = moment(resultOrder.Closed);
    }

    this.logAction('getOrder', 'result', {price, amount, date});
    callback(err, {price, amount, date});
  }.bind(this);

  this.bittrexApi.getorder({uuid: order}, get);
}

Trader.prototype.cancelOrder = function(order, callback) {
  var args = _.toArray(arguments);
  var cancel = function(result, err) {
    this.logAction('cancelOrder', 'called', order);

    if(err || !result.success) {
      log.error('unable to cancel order', order, '(', err, result, '), retrying');
      return this.retry(this.cancelOrder, args);
    }

    this.logAction('getOrder', 'result', result);

    callback();
  }.bind(this);

  this.bittrexApi.cancel({uuid: order}, cancel);
}

Trader.prototype.getTrades = function(since, callback, descending) {

  var firstFetch = !!since;

  var args = _.toArray(arguments);
  var process = function(data, err) {
    if(err) {
      return this.retry(this.getTrades, args);
    }

    let result = data.result;

    // Edge case, see here:
    // @link https://github.com/askmike/gekko/issues/479
    if(firstFetch && _.size(result) === 50000)
      util.die(
        [
          'Poloniex did not provide enough data. Read this:',
          'https://github.com/askmike/gekko/issues/479'
        ].join('\n\n')
      );

      result = _.map(result, function(trade) {
        let mr = {
            tid: trade.Id,
            amount: +trade.Quantity,
            date: moment.utc(trade.TimeStamp).unix(),
            timeStamp: trade.TimeStamp,
            price: +trade.Price
        };
    	return mr;
    });

    callback(null, result.reverse());
  }.bind(this);

  var params = {
    currencyPair: joinCurrencies(this.currency, this.asset)
  }

  if(since)
    params.start = since.unix();

  this.bittrexApi.getmarkethistory({ market: params.currencyPair }, process);
}

Trader.getCapabilities = function () {
  return {
    name: 'bittrex',
    slug: 'bittrex',
    currencies: ['BTC', 'ETH', 'USDT'],
    assets: [
      'BTC', 'BCC','ETH','ANS','BCC'
    ],
    markets: [
      // *** BTC <-> XXX
      { pair: ['BTC', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['BTC', 'ANS'], minimalOrder: { amount: 0.0001, unit: 'asset' } },

      // *** USDT <-> XXX
      { pair: ['USDT', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['USDT', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['USDT', 'ETH'], minimalOrder: { amount: 0.0001, unit: 'asset' } },

      // *** ETH <-> XXX
      { pair: ['ETH', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['ETH', 'ANS'], minimalOrder: { amount: 0.0001, unit: 'asset' } }

    ],
    requires: ['key', 'secret'],
    tid: 'tid',
    providesHistory: 'date',
    providesFullHistory: true,
    tradable: true
  };
}

Trader.prototype.logAction = function(method, msg, obj) {
  console.info(this.name +' ' + method + ': ' + msg);
  if(_.isObject(obj)) {
    console.info(obj);
  }
}

module.exports = Trader;
<commit_msg>additional logging for buy and sell<commit_after>var Bittrex = require('node.bittrex.api');
var util = require('../core/util.js');
var _ = require('lodash');
var moment = require('moment');
var log = require('../core/log');
var apiKeyManager = require('../web/apiKeyManager.js');

// Helper methods
function joinCurrencies(currencyA, currencyB){
    return currencyA + '-' + currencyB;
}

var Trader = function(config) {
  _.bindAll(this);

  // not nice but works to use a web module here to grab the keys
  var keys = apiKeyManager._getApiKeyPair('bittrex');

  if(_.isObject(keys)) {
    config.key = keys.key;
    config.secret = keys.secret;
  } else {
    // no api key defined -> we need to set a dummy key, otherwise the Bittrex module will not work even for public requests
    config.key = 'sdfsdf32424dfdb';
    config.secret = 'sdfsdf32424dfdb';
  }

  if(_.isObject(config)) {
    this.key = config.key;
    this.secret = config.secret;
    this.currency = config.currency;
    this.asset = config.asset;
  }

  this.name = 'Bittrex';
  this.balance;
  this.price;

  this.pair = [this.currency, this.asset].join('-');

  Bittrex.options({
    'apikey':  this.key,
    'apisecret': this.secret,
    'stream': false,
    'verbose': false,
    'cleartext': false
  });

  this.logAction('Init', 'New Trader', {currency: this.currency, asset: this.asset});

  this.bittrexApi = Bittrex;
}

// if the exchange errors we try the same call again after
// waiting 10 seconds
Trader.prototype.retry = function(method, args) {
  var wait = +moment.duration(10, 'seconds');
  log.debug(this.name, 'returned an error, retrying..');

  this.logAction('retry', 'returned an error, retrying..');

  var self = this;

  // make sure the callback (and any other fn)
  // is bound to Trader
  _.each(args, function(arg, i) {
    if(_.isFunction(arg))
      args[i] = _.bind(arg, self);
  });

  // run the failed method again with the same
  // arguments after wait
  setTimeout(
    function() { method.apply(self, args) },
    wait
  );
}

Trader.prototype.getPortfolio = function(callback) {
  var args = _.toArray(arguments);
  this.logAction('getPortfolio', 'called');

  var set = function(data, err) {
    if(err)
      return this.retry(this.getPortfolio, args);

    data = data.result;

    var assetEntry = _.find(data, function(i) { return i.Currency == this.asset}.bind(this));
    var currencyEntry = _.find(data, function(i) { return i.Currency == this.currency}.bind(this));

    if(_.isUndefined(assetEntry)) {
      assetEntry = {
        Available: 0.0,
        Currency: this.asset
      }
    }

    if(_.isUndefined(currencyEntry)) {
      currencyEntry = {
        Available: 0.0,
        Currency: this.currency
      }
    }

    var assetAmount = parseFloat( assetEntry.Available );
    var currencyAmount = parseFloat( currencyEntry.Available );

    if(
      !_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
      !_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
    ) {
      log.info('asset:', this.asset);
      log.info('currency:', this.currency);
      log.info('exchange data:', data);
      util.die('Gekko was unable to set the portfolio');
    }

    var portfolio = [
      { name: this.asset, amount: assetAmount },
      { name: this.currency, amount: currencyAmount }
    ];

    this.logAction('getPortfolio', 'result:', portfolio);

    callback(err, portfolio);
  }.bind(this);

  this.bittrexApi.getbalances(set);
}

Trader.prototype.getTicker = function(callback) {
  var args = _.toArray(arguments);

  this.logAction('getTicker', 'called');

  this.bittrexApi.getticker({market: this.pair}, function(data, err) {
    if(err)
      return this.retry(this.getTicker, args);

    var tick = data.result;

    this.logAction('getTicker', 'result', tick);

    callback(null, {
      bid: parseFloat(tick.Bid),
      ask: parseFloat(tick.Ask),
    })

  }.bind(this));
}

Trader.prototype.getFee = function(callback) {

  this.logAction('getFee', 'called');
  /*var set = function(data, err) {
    if(err || data.error)
      return callback(err || data.error);

    data = data.result;

    var assetEntry = _.find(data, function(c) { return c.Currency == this.asset}.bind(this));
    //var currencyEntry = _.find(data, function(c) { return c.Currency == this.currency}.bind(this));

    var fee = parseFloat(assetEntry.TxFee);

    callback(false, parseFloat(assetEntry.TxFee));
  }.bind(this);

  this.bittrexApi.getcurrencies(set); */

   callback(false, parseFloat(0.00025));
}

Trader.prototype.buy = function(amount, price, callback) {
  var args = _.toArray(arguments);

   this.logAction('buy', 'called', {amoutn: amount, price: price});

  var set = function(result, err) {
    if(err || result.error) {
     // log.error('unable to buy:', err, result);
      this.logAction('unable to buy:', 'err', err);
       this.logAction('unable to buy:', 'result', result);
      return this.retry(this.buy, args);
    }

    this.logAction('buy', 'result', result);

    callback(null, result.result.uuid);
  }.bind(this);

  this.bittrexApi.buylimit({market: this.pair, quantity: amount, rate: price}, set);
}

Trader.prototype.sell = function(amount, price, callback) {
  var args = _.toArray(arguments);

  this.logAction('sell', 'called', {amoutn: amount, price: price});

  var set = function(result, err) {
    if(err || result.error) {
       this.logAction('unable to sell:', 'err', err);
       this.logAction('unable to sell:', 'result', result);
      return this.retry(this.sell, args);
    }

   this.logAction('sell', 'result', result);

    callback(null, result.result.uuid);
  }.bind(this);

  this.bittrexApi.selllimit({market: this.pair, quantity: amount, rate: price}, set);
}

Trader.prototype.checkOrder = function(order, callback) {
  var check = function(result, err) {
    this.logAction('checkOrder', 'called');

    var stillThere = _.find(result.result, function(o) { return o.OrderUuid === order });

    this.logAction('checkOrder', 'result', stillThere);
    callback(err, !stillThere);
  }.bind(this);

  this.bittrexApi.getopenorders({market: this.pair}, check);
}

Trader.prototype.getOrder = function(order, callback) {

  var get = function(result, err) {

    this.logAction('getOrder', 'called');

    if(err)
      return callback(err);

    var price = 0;
    var amount = 0;
    var date = moment(0);

    if(!result.success)
      return callback(null, {price, amount, date});

    var resultOrder = result.result;

    price = resultOrder.Price;
    amount = resultOrder.Quantity;

    if(resultOrder.IsOpen) {
         date = moment(resultOrder.Opened);
    } else {
       date = moment(resultOrder.Closed);
    }

    this.logAction('getOrder', 'result', {price, amount, date});
    callback(err, {price, amount, date});
  }.bind(this);

  this.bittrexApi.getorder({uuid: order}, get);
}

Trader.prototype.cancelOrder = function(order, callback) {
  var args = _.toArray(arguments);
  var cancel = function(result, err) {
    this.logAction('cancelOrder', 'called', order);

    if(err || !result.success) {
      log.error('unable to cancel order', order, '(', err, result, '), retrying');
      return this.retry(this.cancelOrder, args);
    }

    this.logAction('getOrder', 'result', result);

    callback();
  }.bind(this);

  this.bittrexApi.cancel({uuid: order}, cancel);
}

Trader.prototype.getTrades = function(since, callback, descending) {

  var firstFetch = !!since;

  var args = _.toArray(arguments);
  var process = function(data, err) {
    if(err) {
      return this.retry(this.getTrades, args);
    }

    let result = data.result;

    // Edge case, see here:
    // @link https://github.com/askmike/gekko/issues/479
    if(firstFetch && _.size(result) === 50000)
      util.die(
        [
          'Poloniex did not provide enough data. Read this:',
          'https://github.com/askmike/gekko/issues/479'
        ].join('\n\n')
      );

      result = _.map(result, function(trade) {
        let mr = {
            tid: trade.Id,
            amount: +trade.Quantity,
            date: moment.utc(trade.TimeStamp).unix(),
            timeStamp: trade.TimeStamp,
            price: +trade.Price
        };
    	return mr;
    });

    callback(null, result.reverse());
  }.bind(this);

  var params = {
    currencyPair: joinCurrencies(this.currency, this.asset)
  }

  if(since)
    params.start = since.unix();

  this.bittrexApi.getmarkethistory({ market: params.currencyPair }, process);
}

Trader.getCapabilities = function () {
  return {
    name: 'bittrex',
    slug: 'bittrex',
    currencies: ['BTC', 'ETH', 'USDT'],
    assets: [
      'BTC', 'BCC','ETH','ANS','BCC'
    ],
    markets: [
      // *** BTC <-> XXX
      { pair: ['BTC', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['BTC', 'ANS'], minimalOrder: { amount: 0.0001, unit: 'asset' } },

      // *** USDT <-> XXX
      { pair: ['USDT', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['USDT', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['USDT', 'ETH'], minimalOrder: { amount: 0.0001, unit: 'asset' } },

      // *** ETH <-> XXX
      { pair: ['ETH', 'BCC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
      { pair: ['ETH', 'ANS'], minimalOrder: { amount: 0.0001, unit: 'asset' } }

    ],
    requires: ['key', 'secret'],
    tid: 'tid',
    providesHistory: 'date',
    providesFullHistory: true,
    tradable: true
  };
}

Trader.prototype.logAction = function(method, msg, obj) {
  console.info(this.name +' ' + method + ': ' + msg);
  if(_.isObject(obj)) {
    console.info(obj);
  }
}

module.exports = Trader;
<|endoftext|>


<commit_before>/* eslint-disable import/no-named-as-default */
import React, { PropTypes as pt } from 'react';
import { connect } from 'react-redux';
import { Link, locationShape } from 'react-router';
import { push } from 'react-router-redux';
import {
  reduxForm,
  formValueSelector,
  propTypes as reduxFormPropTypes,
  SubmissionError,
} from 'redux-form';
import { pick } from 'underscore';

import {
  makeApiRequest,
  recipeUpdated,
  recipeAdded,
  showNotification,
  setSelectedRecipe,
  userInfoReceived,
  revisionsReceived,
} from 'control/actions/ControlActions';
import {
  getLastApprovedRevision,
} from 'control/selectors/RecipesSelector';
import BooleanIcon from 'control/components/BooleanIcon';
import composeRecipeContainer from 'control/components/RecipeContainer';
import { ControlField } from 'control/components/Fields';
import RecipeFormActions from 'control/components/RecipeFormActions';
import HeartbeatFields from 'control/components/action_fields/HeartbeatFields';
import ConsoleLogFields from 'control/components/action_fields/ConsoleLogFields';
import DraftStatus from 'control/components/DraftStatus';

import JexlEnvironment from 'selfrepair/JexlEnvironment';

export const selector = formValueSelector('recipe');

/**
 * Form for creating new recipes or editing existing recipes.
 */
export class RecipeForm extends React.Component {
  static propTypes = {
    handleSubmit: reduxFormPropTypes.handleSubmit,
    submitting: reduxFormPropTypes.submitting,
    selectedAction: pt.string,
    recipeId: pt.number,
    recipe: pt.shape({
      name: pt.string.isRequired,
      enabled: pt.bool.isRequired,
      filter_expression: pt.string.isRequired,
      action: pt.string.isRequired,
      arguments: pt.object.isRequired,
    }),
    revision: pt.object,
    allRevisions: pt.object,
    recipeFields: pt.object,
    user: pt.object,
    dispatch: pt.func.isRequired,
    // route props passed from router
    route: pt.object,
    routeParams: pt.object.isRequired,
    // from redux-form
    pristine: pt.bool,
  };

  static argumentsFields = {
    'console-log': ConsoleLogFields,
    'show-heartbeat': HeartbeatFields,
  };

  static LoadingSpinner = (

Loading recipe...

  );

  static renderCloningMessage({ route, recipe }) {
    const isCloning = route && route.isCloning;
    if (!isCloning || !recipe) {
      return null;
    }

    return (

        {'You are cloning '}
        <Link to={`/control/recipe/${recipe.id}/`}>
          {recipe.name} ({recipe.action})
        </Link>.

    );
  }

  constructor(props) {
    super(props);

    this.handleFormAction = ::this.handleFormAction;
  }

  componentWillMount() {
    const {
      user,
      dispatch,
    } = this.props;

    if (!user || !user.id) {
      dispatch(makeApiRequest('getCurrentUser'))
        .then(receivedUser => dispatch(userInfoReceived(receivedUser)));
    }
  }

  /**
   * Generates an object relevant to the user/draft state. All values returned
   * are cast as booleans.
   *
   * @return {Object} Hash of user/draft state data, formatted as booleans
   */
  getRenderVariables() {
    const {
      route,
      routeParams = {},
      recipe = {},
      revision = {},
      allRevisions = {},
      pristine,
      submitting,
      recipeId,
      user: {
        id: userId,
      },
    } = this.props;

    const requestDetails = revision.approval_request;
    const currentUserID = userId;
    const isViewingLatestApproved = recipe.approved_revision_id
      && revision.revision_id === recipe.approved_revision_id;
    const hasApprovalRequest = !!requestDetails;
    const requestAuthorID = hasApprovalRequest && requestDetails.creator.id;

    const isUserViewingOutdated = !!routeParams.revisionId;
    const isPendingApproval = hasApprovalRequest && requestDetails.approved === null;
    const isFormDisabled = submitting || (isPendingApproval && !isUserViewingOutdated) || !userId;

    const isAccepted = hasApprovalRequest && requestDetails.approved === true;
    const isRejected = hasApprovalRequest && requestDetails.approved === false;

    const recipeRevisions = recipe ? allRevisions[recipe.id] : {};
    const lastApprovedRevisionId = getLastApprovedRevision(recipeRevisions).revision_id;

    return {
      isCloning: !!(route && route.isCloning),
      isUserRequester: requestAuthorID === currentUserID,
      isAlreadySaved: !!recipeId,
      isFormPristine: pristine,
      isApproved: !!recipeId && recipe.is_approved,
      isEnabled: !!recipeId && recipe.enabled,
      isUserViewingOutdated,
      isViewingLatestApproved,
      isPendingApproval,
      isFormDisabled,
      isAccepted,
      isRejected,
      hasApprovalRequest,
      lastApprovedRevisionId,
    };
  }

  getRecipeHistory(recipeId) {
    const {
      dispatch,
    } = this.props;

    return dispatch(makeApiRequest('fetchRecipeHistory', { recipeId }))
      .then(revisions => {
        dispatch(revisionsReceived({
          recipeId,
          revisions,
        }));
      });
  }

  /**
   * Event handler for form action buttons.
   * Form action buttons remotely fire this handler with a (string) action type,
   * which then triggers the appropriate API requests/etc.
   *
   * @param  {string} action Action type to trigger. ex: 'cancel', 'approve', 'reject'
   */
  handleFormAction(action, data) {
    const {
      recipe,
      revision,
      dispatch,
    } = this.props;

    // Form actions follow the same pattern: call api, show success message, update
    // history. This is just an abstracted handler to keep things DRY.
    const handleAction = ({ apiEndpoint, successMessage, ...apiData }) =>
      dispatch(makeApiRequest(apiEndpoint), apiData || {})
        .then(() => {
          dispatch(showNotification({
            messageType: 'success',
            message: successMessage,
          }));

          this.getRecipeHistory(recipe.id);
        });

    switch (action) {
      case 'cancel':
        return handleAction({
          apiEndpoint: 'closeApprovalRequest',
          successMessage: 'Approval review closed.',
          // Request data
          requestId: revision.approval_request.id,
        });

      case 'approve':
        return handleAction({
          apiEndpoint: 'acceptApprovalRequest',
          successMessage: 'Recipe review successfully approved!',
          // Request data
          requestId: revision.approval_request.id,
          ...data,
        });

      case 'reject':
        return handleAction({
          apiEndpoint: 'rejectApprovalRequest',
          successMessage: 'Recipe review successfully rejected.',
          // Request data
          requestId: revision.approval_request.id,
          ...data,
        });

      case 'request':
        return handleAction({
          apiEndpoint: 'openApprovalRequest',
          successMessage: 'Approval review requested!',
          // Request data
          revisionId: revision.revision_id,
        });

      default:
        throw new Error(`Unrecognized form action "${action}"`);
    }
  }

  render() {
    const {
      handleSubmit,
      selectedAction,
      recipe,
      revision,
      recipeId,
      recipeFields,
    } = this.props;
    const noop = () => null;
    const ArgumentsFields = RecipeForm.argumentsFields[selectedAction] || noop;

    // Show a loading indicator if we haven't yet loaded the recipe.
    if (recipeId && (!recipe && !revision)) {
      return RecipeForm.LoadingSpinner;
    }

    const renderVars = this.getRenderVariables();
    const {
      isFormDisabled,
      lastApprovedRevisionId,
    } = renderVars;

    const thisRevisionRequest = revision && revision.approval_request;

    return (
      <form className="recipe-form" onSubmit={handleSubmit}>
        { RecipeForm.renderCloningMessage(this.props) }
        {
          revision &&
            <DraftStatus
              latestRevisionId={recipe.latest_revision_id}
              lastApprovedRevisionId={lastApprovedRevisionId}
              recipe={revision}
            />
        }

        {
          thisRevisionRequest
          && (thisRevisionRequest.approved === true || thisRevisionRequest.approved === false)
          && (

              This revision has been {renderVars.isAccepted ? 'approved' : 'rejected'}:

                {revision.approval_request.comment}

                  – {revision.approval_request.approver.email}

          )
        }

          <BooleanIcon value={renderVars.isEnabled} />
          { renderVars.isEnabled ? 'Enabled' : 'Disabled' }

        <ControlField
          disabled={isFormDisabled}
          label="Name"
          name="name"
          component="input"
          type="text"
        />
        <ControlField
          disabled={isFormDisabled}
          label="Filter Expression"
          name="extra_filter_expression"
          component="textarea"
        />
        <ControlField
          disabled={isFormDisabled}
          label="Action"
          name="action"
          component="select"
        >
          <option value="">Choose an action...</option>
          <option value="console-log">Log to Console</option>
          <option value="show-heartbeat">Heartbeat Prompt</option>
        </ControlField>

        <ArgumentsFields disabled={isFormDisabled} fields={recipeFields} />

        <RecipeFormActions
          onAction={this.handleFormAction}
          recipeId={recipeId}
          {...renderVars}
        />
      </form>
    );
  }
}

/**
 * Redux-Form config for the RecipeForm.
 */
export const formConfig = {
  form: 'recipe',
  enableReinitialize: true,
  asyncBlurFields: ['extra_filter_expression'],

  async asyncValidate(values) {
    const errors = {};
    // Validate that filter expression is valid JEXL
    if (!values.extra_filter_expression) {
      errors.extra_filter_expression = 'Filter expression cannot be empty.';
    } else {
      const jexlEnvironment = new JexlEnvironment({});
      try {
        await jexlEnvironment.eval(values.extra_filter_expression);
      } catch (err) {
        errors.extra_filter_expression = err.toString();
      }
    }

    // Throw if we found any errors.
    if (Object.keys(errors).length > 0) {
      throw errors;
    }
  },

  onSubmit(values, dispatch, { route, recipeId, updateRecipe, addRecipe }) {
    // Filter out unwanted keys for submission.
    const recipe = pick(values, [
      'name', 'extra_filter_expression', 'action', 'arguments',
    ]);
    const isCloning = route && route.isCloning;

    let result;
    if (recipeId && !isCloning) {
      result = updateRecipe(recipeId, recipe);
    } else {
      result = addRecipe(recipe);
    }

    // Wrap error responses with a SubmissionError for redux-form.
    return result.catch(errors => {
      throw new SubmissionError(errors);
    });
  },

  onSubmitSuccess(result, dispatch) {
    dispatch(showNotification({
      messageType: 'success',
      message: 'Recipe saved.',
    }));
  },

  onSubmitFail(errors, dispatch) {
    dispatch(showNotification({
      messageType: 'error',
      message: 'Recipe cannot be saved. Please correct any errors listed in the form below.',
    }));
  },
};

/**
 * Component wrapper that passes the recipe (or currently selected revision) as
 * the initialValues prop for the form.
 * @param Component Component to wrap.
 */
export function initialValuesWrapper(Component) {
  function Wrapped(props) {
    const { recipe, revision } = props;

    const initialValues = revision || recipe;
    return <Component initialValues={initialValues} {...props} />;
  }
  Wrapped.propTypes = {
    recipe: pt.object,
    revision: pt.object,
    location: locationShape,
  };

  return Wrapped;
}

const connector = connect(
  // Pull selected action from the form state.
  state => ({
    selectedAction: selector(state, 'action'),
    recipeFields: selector(state, 'arguments'),
    user: state.user,
    allRevisions: state.recipes.revisions,
  }),

  // Bound functions for writing to the server.
  dispatch => ({
    addRecipe(recipe) {
      return dispatch(makeApiRequest('addRecipe', { recipe }))
      .then(response => {
        dispatch(recipeAdded(response));
        dispatch(push(`/control/recipe/${response.id}/`));
        dispatch(setSelectedRecipe(response.id));
      });
    },
    updateRecipe(recipeId, recipe) {
      return dispatch(makeApiRequest('updateRecipe', { recipeId, recipe }))
      .then(response => dispatch(recipeUpdated(response)));
    },
  }),
);

// Use reduce to call several wrapper functions in a row.
export default [
  reduxForm(formConfig),
  connector,
  initialValuesWrapper,
  composeRecipeContainer,
].reduce((prev, func) => func(prev), RecipeForm);
<commit_msg>Code review<commit_after>/* eslint-disable import/no-named-as-default */
import React, { PropTypes as pt } from 'react';
import { connect } from 'react-redux';
import { Link, locationShape } from 'react-router';
import { push } from 'react-router-redux';
import {
  reduxForm,
  formValueSelector,
  propTypes as reduxFormPropTypes,
  SubmissionError,
} from 'redux-form';
import { pick } from 'underscore';

import {
  makeApiRequest,
  recipeUpdated,
  recipeAdded,
  showNotification,
  setSelectedRecipe,
  userInfoReceived,
  revisionsReceived,
} from 'control/actions/ControlActions';
import {
  getLastApprovedRevision,
} from 'control/selectors/RecipesSelector';
import BooleanIcon from 'control/components/BooleanIcon';
import composeRecipeContainer from 'control/components/RecipeContainer';
import { ControlField } from 'control/components/Fields';
import RecipeFormActions from 'control/components/RecipeFormActions';
import HeartbeatFields from 'control/components/action_fields/HeartbeatFields';
import ConsoleLogFields from 'control/components/action_fields/ConsoleLogFields';
import DraftStatus from 'control/components/DraftStatus';

import JexlEnvironment from 'selfrepair/JexlEnvironment';

export const selector = formValueSelector('recipe');

/**
 * Form for creating new recipes or editing existing recipes.
 */
export class RecipeForm extends React.Component {
  static propTypes = {
    handleSubmit: reduxFormPropTypes.handleSubmit,
    submitting: reduxFormPropTypes.submitting,
    selectedAction: pt.string,
    recipeId: pt.number,
    recipe: pt.shape({
      name: pt.string.isRequired,
      enabled: pt.bool.isRequired,
      filter_expression: pt.string.isRequired,
      action: pt.string.isRequired,
      arguments: pt.object.isRequired,
    }),
    revision: pt.object,
    allRevisions: pt.object,
    recipeFields: pt.object,
    user: pt.object,
    dispatch: pt.func.isRequired,
    // route props passed from router
    route: pt.object,
    routeParams: pt.object.isRequired,
    // from redux-form
    pristine: pt.bool,
  };

  static argumentsFields = {
    'console-log': ConsoleLogFields,
    'show-heartbeat': HeartbeatFields,
  };

  static LoadingSpinner = (

Loading recipe...

  );

  static renderCloningMessage({ route, recipe }) {
    const isCloning = route && route.isCloning;
    if (!isCloning || !recipe) {
      return null;
    }

    return (

        {'You are cloning '}
        <Link to={`/control/recipe/${recipe.id}/`}>
          {recipe.name} ({recipe.action})
        </Link>.

    );
  }

  constructor(props) {
    super(props);

    this.handleFormAction = ::this.handleFormAction;
  }

  componentWillMount() {
    const {
      user,
      dispatch,
    } = this.props;

    if (!user || !user.id) {
      dispatch(makeApiRequest('getCurrentUser'))
        .then(receivedUser => dispatch(userInfoReceived(receivedUser)));
    }
  }

  /**
   * Generates an object relevant to the user/draft state. All values returned
   * are cast as booleans.
   *
   * @return {Object} Hash of user/draft state data, formatted as booleans
   */
  getRenderVariables() {
    const {
      route,
      routeParams = {},
      recipe = {},
      revision = {},
      allRevisions = {},
      pristine,
      submitting,
      recipeId,
      user: {
        id: userId,
      },
    } = this.props;

    const requestDetails = revision && revision.approval_request;
    const currentUserID = userId;
    const isViewingLatestApproved = recipe && recipe.approved_revision_id
      && revision.revision_id === recipe.approved_revision_id;
    const hasApprovalRequest = !!requestDetails;
    const requestAuthorID = hasApprovalRequest && requestDetails.creator.id;

    const isUserViewingOutdated = !!routeParams.revisionId;
    const isPendingApproval = hasApprovalRequest && requestDetails.approved === null;
    const isFormDisabled = submitting || (isPendingApproval && !isUserViewingOutdated) || !userId;

    const isAccepted = hasApprovalRequest && requestDetails.approved === true;
    const isRejected = hasApprovalRequest && requestDetails.approved === false;

    const recipeRevisions = recipe ? allRevisions[recipe.id] : {};
    const lastApprovedRevisionId = getLastApprovedRevision(recipeRevisions).revision_id;

    return {
      isCloning: !!(route && route.isCloning),
      isUserRequester: requestAuthorID === currentUserID,
      isAlreadySaved: !!recipeId,
      isFormPristine: pristine,
      isApproved: !!recipeId && recipe.is_approved,
      isEnabled: !!recipeId && recipe.enabled,
      isUserViewingOutdated,
      isViewingLatestApproved,
      isPendingApproval,
      isFormDisabled,
      isAccepted,
      isRejected,
      hasApprovalRequest,
      lastApprovedRevisionId,
    };
  }

  getRecipeHistory(recipeId) {
    const {
      dispatch,
    } = this.props;

    return dispatch(makeApiRequest('fetchRecipeHistory', { recipeId }))
      .then(revisions => {
        dispatch(revisionsReceived({
          recipeId,
          revisions,
        }));
      });
  }

  /**
   * Event handler for form action buttons.
   * Form action buttons remotely fire this handler with a (string) action type,
   * which then triggers the appropriate API requests/etc.
   *
   * @param  {string} action Action type to trigger. ex: 'cancel', 'approve', 'reject'
   */
  handleFormAction(action, data) {
    const {
      recipe,
      revision,
      dispatch,
    } = this.props;

    // Form actions follow the same pattern: call api, show success message, update
    // history. This is just an abstracted handler to keep things DRY.
    const handleAction = ({ apiEndpoint, successMessage, ...apiData }) =>
      dispatch(makeApiRequest(apiEndpoint), apiData || {})
        .then(() => {
          dispatch(showNotification({
            messageType: 'success',
            message: successMessage,
          }));

          this.getRecipeHistory(recipe.id);
        });

    switch (action) {
      case 'cancel':
        return handleAction({
          apiEndpoint: 'closeApprovalRequest',
          successMessage: 'Approval review closed.',
          // Request data
          requestId: revision.approval_request.id,
        });

      case 'approve':
        return handleAction({
          apiEndpoint: 'acceptApprovalRequest',
          successMessage: 'Recipe review successfully approved!',
          // Request data
          requestId: revision.approval_request.id,
          ...data,
        });

      case 'reject':
        return handleAction({
          apiEndpoint: 'rejectApprovalRequest',
          successMessage: 'Recipe review successfully rejected.',
          // Request data
          requestId: revision.approval_request.id,
          ...data,
        });

      case 'request':
        return handleAction({
          apiEndpoint: 'openApprovalRequest',
          successMessage: 'Approval review requested!',
          // Request data
          revisionId: revision.revision_id,
        });

      default:
        throw new Error(`Unrecognized form action "${action}"`);
    }
  }

  render() {
    const {
      handleSubmit,
      selectedAction,
      recipe,
      revision,
      recipeId,
      recipeFields,
    } = this.props;
    const noop = () => null;
    const ArgumentsFields = RecipeForm.argumentsFields[selectedAction] || noop;

    // Show a loading indicator if we haven't yet loaded the recipe.
    if (recipeId && (!recipe && !revision)) {
      return RecipeForm.LoadingSpinner;
    }

    const renderVars = this.getRenderVariables();
    const {
      isFormDisabled,
      lastApprovedRevisionId,
    } = renderVars;

    const thisRevisionRequest = revision && revision.approval_request;

    return (
      <form className="recipe-form" onSubmit={handleSubmit}>
        { RecipeForm.renderCloningMessage(this.props) }
        {
          revision &&
            <DraftStatus
              latestRevisionId={recipe.latest_revision_id}
              lastApprovedRevisionId={lastApprovedRevisionId}
              recipe={revision}
            />
        }

        {
          thisRevisionRequest
          && (thisRevisionRequest.approved === true || thisRevisionRequest.approved === false)
          && (

              This revision has been {renderVars.isAccepted ? 'approved' : 'rejected'}:

                {revision.approval_request.comment}

                  – {revision.approval_request.approver.email}

          )
        }

          <BooleanIcon value={renderVars.isEnabled} />
          { renderVars.isEnabled ? 'Enabled' : 'Disabled' }

        <ControlField
          disabled={isFormDisabled}
          label="Name"
          name="name"
          component="input"
          type="text"
        />
        <ControlField
          disabled={isFormDisabled}
          label="Filter Expression"
          name="extra_filter_expression"
          component="textarea"
        />
        <ControlField
          disabled={isFormDisabled}
          label="Action"
          name="action"
          component="select"
        >
          <option value="">Choose an action...</option>
          <option value="console-log">Log to Console</option>
          <option value="show-heartbeat">Heartbeat Prompt</option>
        </ControlField>

        <ArgumentsFields disabled={isFormDisabled} fields={recipeFields} />

        <RecipeFormActions
          onAction={this.handleFormAction}
          recipeId={recipeId}
          {...renderVars}
        />
      </form>
    );
  }
}

/**
 * Redux-Form config for the RecipeForm.
 */
export const formConfig = {
  form: 'recipe',
  enableReinitialize: true,
  asyncBlurFields: ['extra_filter_expression'],

  async asyncValidate(values) {
    const errors = {};
    // Validate that filter expression is valid JEXL
    if (!values.extra_filter_expression) {
      errors.extra_filter_expression = 'Filter expression cannot be empty.';
    } else {
      const jexlEnvironment = new JexlEnvironment({});
      try {
        await jexlEnvironment.eval(values.extra_filter_expression);
      } catch (err) {
        errors.extra_filter_expression = err.toString();
      }
    }

    // Throw if we found any errors.
    if (Object.keys(errors).length > 0) {
      throw errors;
    }
  },

  onSubmit(values, dispatch, { route, recipeId, updateRecipe, addRecipe }) {
    // Filter out unwanted keys for submission.
    const recipe = pick(values, [
      'name', 'extra_filter_expression', 'action', 'arguments',
    ]);
    const isCloning = route && route.isCloning;

    let result;
    if (recipeId && !isCloning) {
      result = updateRecipe(recipeId, recipe);
    } else {
      result = addRecipe(recipe);
    }

    // Wrap error responses with a SubmissionError for redux-form.
    return result.catch(errors => {
      throw new SubmissionError(errors);
    });
  },

  onSubmitSuccess(result, dispatch) {
    dispatch(showNotification({
      messageType: 'success',
      message: 'Recipe saved.',
    }));
  },

  onSubmitFail(errors, dispatch) {
    dispatch(showNotification({
      messageType: 'error',
      message: 'Recipe cannot be saved. Please correct any errors listed in the form below.',
    }));
  },
};

/**
 * Component wrapper that passes the recipe (or currently selected revision) as
 * the initialValues prop for the form.
 * @param Component Component to wrap.
 */
export function initialValuesWrapper(Component) {
  function Wrapped(props) {
    const { recipe, revision } = props;

    const initialValues = revision || recipe;
    return <Component initialValues={initialValues} {...props} />;
  }
  Wrapped.propTypes = {
    recipe: pt.object,
    revision: pt.object,
    location: locationShape,
  };

  return Wrapped;
}

const connector = connect(
  // Pull selected action from the form state.
  state => ({
    selectedAction: selector(state, 'action'),
    recipeFields: selector(state, 'arguments'),
    user: state.user,
    allRevisions: state.recipes.revisions,
  }),

  // Bound functions for writing to the server.
  dispatch => ({
    addRecipe(recipe) {
      return dispatch(makeApiRequest('addRecipe', { recipe }))
      .then(response => {
        dispatch(recipeAdded(response));
        dispatch(push(`/control/recipe/${response.id}/`));
        dispatch(setSelectedRecipe(response.id));
      });
    },
    updateRecipe(recipeId, recipe) {
      return dispatch(makeApiRequest('updateRecipe', { recipeId, recipe }))
      .then(response => dispatch(recipeUpdated(response)));
    },
  }),
);

// Use reduce to call several wrapper functions in a row.
export default [
  reduxForm(formConfig),
  connector,
  initialValuesWrapper,
  composeRecipeContainer,
].reduce((prev, func) => func(prev), RecipeForm);
<|endoftext|>


<commit_before>/*
 * β Bilanx, module to handle bindings between commands and Redis replies,
 * for Deuces, a minimal Redis client.
 * Moreover, it uses an automatic rollback mechanism for subscriptions.
 *
 * Copyright(c) 2014 Guglielmo Ferri <44gatti@gmail.com>
 * MIT Licensed
 */

exports.version = require( '../package' ).version;
exports.Bilanx = ( function () {
    var log = console.log
        , util = require( 'util' )
        , Bolgia = require( 'bolgia' )
        , doString = Bolgia.doString
        , clone = Bolgia.clone
        , improve = Bolgia.improve
        , ooo = Bolgia.circles
        , oobj = ooo.obj
        , oarr = ooo.arr
        , Train = require( 'train' )
        , reset = function ( obj ) {
            var me = this
                , h = obj || {}
                , p = null
                , type = null
                ;
            for ( p in h ) {
                type = doString( h[ p ] );
                if ( type === oobj ) {
                    reset( h[ p ] );
                    continue;
                }
                if ( type === oarr ) {
                    h[ p ] = [];
                    continue;
                }
                h[ p ] = 0;
            };
        }
        , sendError = function ( ocmd, emsg ) {
            var err = new Error( emsg )
                ;
            ocmd.err = err;
            // execute callback
            ocmd.zn( true, err.message, ocmd.fn );
            return -1;
        }
        , bilanx_opt = {
            timestamps : false
            , rollback : 64 * 1024
        }
        // Bilanx
        , Bilanx = function ( opt ) {
            var me = this
                , is = me instanceof Bilanx
                ;
            if ( ! is ) return new Bilanx();
            // set options
            me.options = improve( clone( opt ), bilanx_opt );
            // set internal queue with limits
            me.cqueue = Train( {
                rlim : 64 * 1024
                , xlim : Infinity
            } );
            // set internal status
            me.status = {
                subscription : {
                    on : 0
                    , active : 0
                    , channels : 0
                    , patterns : 0
                }
                , monitoring : {
                    on : 0
                    , active : 0
                }
                , auth : []
                , last_access = 0
            }
            ;
        }
        , bproto = Bilanx.prototype
        ;

    bproto.flush = function () {
        var me = this
            , cqueue = me.cqueue
            , status = me.status
            ;
        cqueue.flush();
        reset( status );
        cqueue.rollUp( false );
        return me;
    };

    bproto.rollBack = function ( bool ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.rollBack( bool );
        return me;
    };

    bproto.rollUp = function ( bool ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.rollUp( bool );
        return me;
    };

    bproto.auth = function ( cmd ) {
        var me = this
            , ocmd = doString( cmd ) === oobj ? cmd : null
            ;
        return ( me.status.auth.push( ocmd ) && ocmd.isAuth ) ? ocmd : null;
    };

    bproto.push = function ( ocmd ) {
        var me = this
            , status = me.status
            , s = status.subscription
            , m = status.monitoring
            , cqueue = me.cqueue
            , cname = ocmd.cmd
            ;

        if ( ocmd.isQuit ) return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
        if ( m.on ) return sendError( ocmd, 'only the QUIT command is allowed in monitor mode.' );
        if ( s.on ) {
            // Redis >= 2.8.x permits PING in pubsub mode
            if ( ocmd.isPing ) return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
            if( ! ocmd.isSubscription ) return sendError( ocmd, cname + ' command is not allowed in PubSub mode.' );
        }
        if ( ocmd.isMonitor ) m.on = 1;
        else if ( ocmd.isSubscription ) s.on = 1;
        /*
         * There is only a single argument to push, use
         * the faster way to do it, instead of using
         * cqueue.push( ocmd ). See Train#fpush.
         */
        return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
    };

    bproto.head = function ( pos ) {
        var me = this
            , status =  me.status
            , tstamp = me.options,timestamps
            , auth = status.auth[ 0 ]
            ;
        // update last access_time
        if ( tstamp ) status.last_access = Date.now();
        return auth ? auth : me.cqueue.get( + pos || 0 );
    };

    bproto.pop = function () {
        var me = this
            , status = me.status
            , m = status.monitoring
            , s = status.subscription
            , tstamp = me.options,timestamps
            , auth = status.auth[ 0 ]
            , cqueue = me.cqueue
            // get auth command if exists or current command enqueued
            , curr = cqueue.get( 0 )
            ;
        // update last access_time
        if ( tstamp ) status.last_access = Date.now();
        // check auth
        if ( auth ) {
            // return stored command AUTH and reset status property
            status.auth = [];
            return auth;
        }
        // if no cmd was queued, a message was received
        if ( ! curr ) return;
        if ( curr.isQuit ) {
            /*
             * QUIT command, reset internal status
             * and disable the rollUp mechanism.
             */
            reset( status );
            cqueue.rollUp( false );
            return cqueue.shift();
        }
        if ( m.active || curr.isMonitor ) {
            m.active = 1;
            // return the current command or undefined
            return cqueue.shift();
        }
        if ( curr.isSubscription ) {
            s.active = 1;
            // enable rollUp only if it is not yet activated
            if ( ! cqueue.roll ) cqueue.rollUp( true );
            /*
             * check if (P)UNSUBSCRIBE without arguments, set the
             * expectedMessages property to active channels or patterns.
             */
            if ( curr.bulks === 1 ) curr.expectedMessages = curr.isUnsubscribe ?
               ( s.channels ? s.channels : 0 ) :
               ( curr.isPunsubscribe ? ( s.patterns ? s.patterns : 0 ) : 0 )
               ;
            // check for expected messages, don't pop command if !== 0
            if ( curr.expectedMessages ) return --curr.expectedMessages ? curr : cqueue.shift();
        }
        return cqueue.shift();
    };

    bproto.subs = function () {
        var me = this
            , s = me.status.subscription
            ;
        return s.on ? s.channels + s.patterns : 0;
    };

    bproto.update = function ( scmd, total_curr_subs ) {
        var me = this
            , status = me.status
            , s = status.subscription
            , tot = s.channels + s.patterns
            ;
        switch ( scmd ) {
            case 'subscribe':
                if ( tot < total_curr_subs ) ++s.channels;
            break;
            case 'unsubscribe':
                if ( tot > total_curr_subs ) --s.channels;
            break;
            case 'psubscribe':
                if ( tot < total_curr_subs ) ++s.patterns;
            break;
            case 'punsubscribe':
                if ( tot > total_curr_subs ) --s.patterns;
            break;
            default:
            break;
        };
        tot = s.channels + s.patterns;
        s.active = s.on = + !! tot;
        // disable rollUp if pubsub mode is off
        if ( ! s.active ) me.rollUp( false );
        return tot;
    };

    bproto.iterate = function ( fn, scope, cback, evict ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.iterate.apply( cqueue, arguments );
        return me;
    };

    bproto.reset = function () {
        var me = this
            ;
        reset( me.status );
        return me;
    };

    return Bilanx;

} )();<commit_msg>v0.8.2. Fixed v0.8.1. On branch master modified:   lib/bilanx.js<commit_after>/*
 * β Bilanx, module to handle bindings between commands and Redis replies,
 * for Deuces, a minimal Redis client.
 * Moreover, it uses an automatic rollback mechanism for subscriptions.
 *
 * Copyright(c) 2014 Guglielmo Ferri <44gatti@gmail.com>
 * MIT Licensed
 */

exports.version = require( '../package' ).version;
exports.Bilanx = ( function () {
    var log = console.log
        , util = require( 'util' )
        , Bolgia = require( 'bolgia' )
        , doString = Bolgia.doString
        , clone = Bolgia.clone
        , improve = Bolgia.improve
        , ooo = Bolgia.circles
        , oobj = ooo.obj
        , oarr = ooo.arr
        , Train = require( 'train' )
        , reset = function ( obj ) {
            var me = this
                , h = obj || {}
                , p = null
                , type = null
                ;
            for ( p in h ) {
                type = doString( h[ p ] );
                if ( type === oobj ) {
                    reset( h[ p ] );
                    continue;
                }
                if ( type === oarr ) {
                    h[ p ] = [];
                    continue;
                }
                h[ p ] = 0;
            };
        }
        , sendError = function ( ocmd, emsg ) {
            var err = new Error( emsg )
                ;
            ocmd.err = err;
            // execute callback
            ocmd.zn( true, err.message, ocmd.fn );
            return -1;
        }
        , bilanx_opt = {
            timestamps : false
            , rollback : 64 * 1024
        }
        // Bilanx
        , Bilanx = function ( opt ) {
            var me = this
                , is = me instanceof Bilanx
                ;
            if ( ! is ) return new Bilanx();
            // set options
            me.options = improve( clone( opt ), bilanx_opt );
            // set internal queue with limits
            me.cqueue = Train( {
                rlim : 64 * 1024
                , xlim : Infinity
            } );
            // set internal status
            me.status = {
                subscription : {
                    on : 0
                    , active : 0
                    , channels : 0
                    , patterns : 0
                }
                , monitoring : {
                    on : 0
                    , active : 0
                }
                , auth : []
                , last_access : 0
            }
            ;
        }
        , bproto = Bilanx.prototype
        ;

    bproto.flush = function () {
        var me = this
            , cqueue = me.cqueue
            , status = me.status
            ;
        cqueue.flush();
        reset( status );
        cqueue.rollUp( false );
        return me;
    };

    bproto.rollBack = function ( bool ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.rollBack( bool );
        return me;
    };

    bproto.rollUp = function ( bool ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.rollUp( bool );
        return me;
    };

    bproto.auth = function ( cmd ) {
        var me = this
            , ocmd = doString( cmd ) === oobj ? cmd : null
            ;
        return ( me.status.auth.push( ocmd ) && ocmd.isAuth ) ? ocmd : null;
    };

    bproto.push = function ( ocmd ) {
        var me = this
            , status = me.status
            , s = status.subscription
            , m = status.monitoring
            , cqueue = me.cqueue
            , cname = ocmd.cmd
            ;

        if ( ocmd.isQuit ) return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
        if ( m.on ) return sendError( ocmd, 'only the QUIT command is allowed in monitor mode.' );
        if ( s.on ) {
            // Redis >= 2.8.x permits PING in pubsub mode
            if ( ocmd.isPing ) return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
            if( ! ocmd.isSubscription ) return sendError( ocmd, cname + ' command is not allowed in PubSub mode.' );
        }
        if ( ocmd.isMonitor ) m.on = 1;
        else if ( ocmd.isSubscription ) s.on = 1;
        /*
         * There is only a single argument to push, use
         * the faster way to do it, instead of using
         * cqueue.push( ocmd ). See Train#fpush.
         */
        return cqueue.qtail.push( ocmd ) + cqueue.qhead.length - cqueue.hpos;
    };

    bproto.head = function ( pos ) {
        var me = this
            , status =  me.status
            , tstamp = me.options,timestamps
            , auth = status.auth[ 0 ]
            ;
        // update last access_time
        if ( tstamp ) status.last_access = Date.now();
        return auth ? auth : me.cqueue.get( + pos || 0 );
    };

    bproto.pop = function () {
        var me = this
            , status = me.status
            , m = status.monitoring
            , s = status.subscription
            , tstamp = me.options,timestamps
            , auth = status.auth[ 0 ]
            , cqueue = me.cqueue
            // get auth command if exists or current command enqueued
            , curr = cqueue.get( 0 )
            ;
        // update last access_time
        if ( tstamp ) status.last_access = Date.now();
        // check auth
        if ( auth ) {
            // return stored command AUTH and reset status property
            status.auth = [];
            return auth;
        }
        // if no cmd was queued, a message was received
        if ( ! curr ) return;
        if ( curr.isQuit ) {
            /*
             * QUIT command, reset internal status
             * and disable the rollUp mechanism.
             */
            reset( status );
            cqueue.rollUp( false );
            return cqueue.shift();
        }
        if ( m.active || curr.isMonitor ) {
            m.active = 1;
            // return the current command or undefined
            return cqueue.shift();
        }
        if ( curr.isSubscription ) {
            s.active = 1;
            // enable rollUp only if it is not yet activated
            if ( ! cqueue.roll ) cqueue.rollUp( true );
            /*
             * check if (P)UNSUBSCRIBE without arguments, set the
             * expectedMessages property to active channels or patterns.
             */
            if ( curr.bulks === 1 ) curr.expectedMessages = curr.isUnsubscribe ?
               ( s.channels ? s.channels : 0 ) :
               ( curr.isPunsubscribe ? ( s.patterns ? s.patterns : 0 ) : 0 )
               ;
            // check for expected messages, don't pop command if !== 0
            if ( curr.expectedMessages ) return --curr.expectedMessages ? curr : cqueue.shift();
        }
        return cqueue.shift();
    };

    bproto.subs = function () {
        var me = this
            , s = me.status.subscription
            ;
        return s.on ? s.channels + s.patterns : 0;
    };

    bproto.update = function ( scmd, total_curr_subs ) {
        var me = this
            , status = me.status
            , s = status.subscription
            , tot = s.channels + s.patterns
            ;
        switch ( scmd ) {
            case 'subscribe':
                if ( tot < total_curr_subs ) ++s.channels;
            break;
            case 'unsubscribe':
                if ( tot > total_curr_subs ) --s.channels;
            break;
            case 'psubscribe':
                if ( tot < total_curr_subs ) ++s.patterns;
            break;
            case 'punsubscribe':
                if ( tot > total_curr_subs ) --s.patterns;
            break;
            default:
            break;
        };
        tot = s.channels + s.patterns;
        s.active = s.on = + !! tot;
        // disable rollUp if pubsub mode is off
        if ( ! s.active ) me.rollUp( false );
        return tot;
    };

    bproto.iterate = function ( fn, scope, cback, evict ) {
        var me = this
            , cqueue = me.cqueue
            ;
        cqueue.iterate.apply( cqueue, arguments );
        return me;
    };

    bproto.reset = function () {
        var me = this
            ;
        reset( me.status );
        return me;
    };

    return Bilanx;

} )();<|endoftext|>