Source: utils/chapi.js

/**
 *  @overview this library provides several shared functions that are used
 *            throughout the cox-chapi module
 *  @author Ben Watson <ben.watson@coxautoinc.com>
 */

var fs = require('fs');
var https = require('https');
var child_process = require('child_process');

/**
 *  @namespace module:cox-chapi.utils
 */
var utils = {
  _get_settings,
  _options,
  _parse_stdin_data,
  _set_settings,
  execute,
  find_api_key,
  find_cache,
  find_creds,
  get_package_json,
  parse_chapi,
  print_response,
  read_stdin,
  run,
  send_request,
  set_api_key,
  set_cache,
  set_creds,
};

/**
 *  gets the settings object by reading from a settings file
 *  @memberof module:cox-chapi.utils
 *  @private
 *  @param {objectCallback} cb - called with the settings object
 */
function _get_settings(cb) {
  fs.readFile(process.env['HOME'] + '/.cloudhealthapi.json', (err, content) => {
    if (err) {
      if (err.code === 'ENOENT') {
        this._set_settings({}, cb);
      }
      else {
        cb(err, content);
      }
    }
    else {
      var settings = JSON.parse(content);
      cb(null, settings);
    }
  });
}

/**
 *  creates and returns an options object that can be given to utils.send_request
 *  @private
 *  @param {string} base - the base path
 *  @param {string} method - the method to use (ie. GET, POST, etc.)
 *  @param {string|null} path - a string to append to the path field of options
 *  @param {string[]|null} params - an array of parameters to add to the url in the
 *    form "key=value"
 *  @param {string} api_key - the api_key
 *  @return {object} an options object
 */
function _options(base, method, path, params, api_key) {
  if (!path) path = '';
  if (!params) params = [];
  params.push('api_key=' + api_key);

  var start = true;
  while (params.length) {
    if (start) {
      path += '?';
      start = false;
    }
    else path += '&';

    path += params.shift();
  }

  return {
    host: 'chapi.cloudhealthtech.com',
    port: 443,
    path: base + path,
    method: method.toUpperCase(),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
  };
}

/**
 *  parses a string of data as parameters given to the chapi command-line utility
 *  @private
 *  @memberof module:cox-chapi.utils
 *  @param {string} data - the string containing parameter data
 *  @return {string[]} an array of parameters
 */
function _parse_stdin_data(data) {
  var params;
  // if data should be treated as json
  if (data.trim().match(/^\{.*\}$/)) {
    params = [data];
  }
  // split the data by spaces
  else {
    params = data.split(/\s+/);
  }

  // clean empty elements on the ends that resulted from calling "split"
  if(params.length && params[params.length - 1] == '') {
    params.pop();
  }
  if(params.length && params[0] == '') {
    params.shift();
  }
  return params;
}

/**
 *  sets the settings object by writing to a settings file
 *  @memberof module:cox-chapi.utils
 *  @private
 *  @param {object} settings - the new settings object
 *  @param {objectCallback} [cb] - called with the new settings object
 */
function _set_settings(settings, cb) {
  fs.open(process.env['HOME'] + '/.cloudhealthapi.json', 'w', (err1, fd) => {
    if (err1) {
      if (cb) return cb(err1);
      else throw err1;
    }

    if (!settings.cache) {
      settings.cache = {};
    }
    if (!settings.creds) {
      settings.creds = {};
    }

    fs.write(fd, JSON.stringify(settings), (err2) => {
      if (err2) {
        if (cb) return cb(err2);
        else throw err2;
      }
      cb(null, settings);
    });
  });
}

/**
 *  Executes func on component with params as parameters
 *  @memberof module:cox-chapi.utils
 *  @param {object} component
 *  @param {function} func
 *  @param {string[]} params
 *  @param {mixedCallback} [cb] - called with the result of func (required if func takes a callback)
 */
function execute(component, func, params, cb) {
  params = params.slice();
  if (params.length > 0) {
    params = this.parse_chapi(params);
  }

  params.push(cb);
  var response = func.apply(component, params);
  if (response !== undefined && response !== null) {
    if (response instanceof Error) {
      this.print_response(response);
    }
    else {
      this.print_response(null, response);
    }
  }
}

/**
 *  gets the api_key for the current environment
 *  @memberof module:cox-chapi.utils
 *  @param {objectCallback} cb - called with the api_key
 */
function find_api_key(cb) {
  var api_key = process.env['CHAPI_KEY'];
  if (api_key) {
    cb(null, api_key);
  }
  else {
    this.find_creds((err, creds) => {
      if (err) return cb(err, creds);
      if (creds.api_key) {
        cb(null, creds.api_key);
      }
      else {
        cb(new Error('No API key found. Make sure to set CHAPI_KEY to your api_key'));
      }
    });
  }
}

/**
 *  gets the cache with the given cache_name from the settings file if it exists
 *  @memberof module:cox-chapi.utils
 *  @param {mixedCallback} cb - called with the cache value
 */
function find_cache(cache_name, cb) {
  this._get_settings((err, settings) => {
    if (err) return cb(err);
    cb(null, settings.cache[cache_name]);
  });
}

/**
 *  gets the creds object from the settings file if it exists
 *  @deprecated use find_api_key instead
 *  @memberof module:cox-chapi.utils
 *  @param {objectCallback} cb - called with the credentials object
 */
function find_creds(cb) {
  this._get_settings((err, settings) => {
    if (err) return cb(err);
    cb(null, settings.creds);
  });
}

/**
 *  Gets the package.json file
 *  @param {objectCallback} cb - yields an object containing package.json data
 */
function get_package_json(cb) {
  fs.readFile(`${__dirname}/../package.json`, (err, contents) => {
    if (err) return cb(err, contents);
    var json;
    try {
      json = JSON.parse(contents);
    }
    catch (e) {
      return cb(e, contents);
    }
    cb(null, json);
  })
}

/**
 *  parses an array of key-value pairs (ie. --key=value) into a javscript object
 *  @memberof module:cox-chapi.utils
 *  @param {string[]} params - an array of chapi strings of the form "--key=value"
 *  @return {Array} an array whose first parameter is an object containing the
 *                  key-value pairs, and the remaining parameters are any non-key-value
 *                  pairs in the order that they were given
 */
function parse_chapi(params) {
  var obj = {};
  var new_params = [];
  for (var i=0; i<params.length; i++) {
    var param = params[i];
    // if params is not a key-value, don't parse it
    if (!param.match(/^--.+/)) {
      // if param is json, parse the json
      if (param.match(/^\s*\{.*\}\s*$/)) {
        param = JSON.parse(param);
      }
      new_params.push(param);
    }
    else {
      param = param.slice(2);
      var key, value;
      if (param.match(/=/)) {
        var pair = param.split('=');
        key = pair[0], value = pair[1];
      }
      else {
        key = param;
        value = 'true';
      }
      obj[key] = value;

      // prepend the obj to the array, if if doesn't contain it already
      if (!new_params.length || new_params[0] !== obj) {
        new_params.unshift(obj);
      }
    }
  }
  return new_params;
}

/**
 *  prints the contents of a json object or an error message. Intended
 *  as a callback
 *  @memberof module:cox-chapi.utils
 *  @param {object} err
 *  @param {mixed} res
 */
function print_response(err, res) {
  if (err) {
    if (res) console.error(res);
    if (process.env['CHAPI_DEV_MODE']) throw err;
    else {
      console.error(err.message);
      process.exit(1);
    }
  }
  else {
    console.log(JSON.stringify(res));
  }
}

/**
 *  Reads from stdin and sends data through callback
 *  @memberof module:cox-chapi.utils
 *  @param {stringCallback} cb - called with the data that was read in
 */
function read_stdin(cb) {
  var data = '';
  process.stdin.setEncoding('utf8');

  process.stdin.on('data', function(chunk) {
    data += chunk;
  });

  process.stdin.on('end', function() {
    cb(null, data);
  });

  process.stdin.on('error', function(err) {
    cb(err);
  });
}

/**
 *  executes a script in the scripts folder with the given arguments
 *  @memberof module:cox-chapi.utils
 *  @param {string} script - the name of the script to run (without .js)
 *  @return {EventEmitter} an EventEmitter for the child process
 */
function run(script, ...args) {
  var path = `${__dirname}/../scripts/${script}.js`;

  var invoked = false;

  try {
    fs.accessSync(path, fs.F_OK);
  }
  catch (err) {
    this.print_response(new Error(`${script}.js is not an accessible script`));
    return;
  }
  var script_process = child_process.fork(path, args);

  script_process.on('error', (err) => {
    if (invoked) return;
    invoked = true;
    this.print_response(err);
  });

  script_process.on('exit', (code) => {
    if (invoked) return;
    invoked = true;
    if (code !== 0) {
      this.print_response(new Error(`script ${script} exited with exit code ${code}`));
    }
  });

  return script_process;
}

/**
 *  function for sending an HTTPS call for CloudHealth's API
 *  @memberof module:cox-chapi.utils
 *  @param {object} [flags] - an optional flags object
 *  @param {boolean} [flags.headers] - if true, yield only the headers returned
 *  @param {object} options - options to use for the https.request function
 *  @param {string} send_data - data to send
 *  @param {objectCallback} cb - called with the parsed json response body
 */
function send_request(flags, options, send_data, cb) {
  if (typeof send_data === 'function') {
    cb = send_data;
    send_data = options;
    options = flags;
    flags = null;
  }

  if (send_data && typeof send_data !== 'string') {
    send_data = JSON.stringify(send_data);
  }

  var req = https.request(options, function(res) {
    res.setEncoding('utf8');

    if (flags && flags.headers) {
      cb(null, res.headers);
      return;
    }

    var data = '';

    res.on('data', function(chunk) {
      data = data + chunk;
    });

    res.on('end', function() {
      var json = data;
      if (data !== '') {
        json = JSON.parse(data);
      }

      if((res.statusCode < 200) || (res.statusCode >= 300)) {
        cb(new Error('Request received status code: ' + res.statusCode + '\n'), json);
      }
      else {
        cb(null, json);
      }
    });
  });

  req.on('error', function(message) {
    cb(new Error(message));
  });

  if((send_data !== null) && (typeof send_data !== 'undefined')) {
    req.write(send_data);
  }
  req.end();
};

/**
 *  sets the api_key for an environment
 *  @memberof module:cox-chapi.utils
 *  @param {string} api_key - the api_key to use for CloudHealth
 *  @param {objectCallback} [cb] - called with the api_key
 */
function set_api_key(api_key, cb) {
  if (!api_key) {
    return cb(new Error('No api_key given'), api_key);
  }
  process.env['CHAPI_KEY'] = api_key;
  this.set_creds(api_key, (err, creds) => {
    if (err) return cb(err, creds);
    cb(null, api_key);
  });
}

/**
 *  sets a cache with the given cache_name object by writing to a settings file
 *  @memberof module:cox-chapi.utils
 *  @param {string} cache_name - the name of the cache variable to set
 *  @param {mixed} cache - the new cache variable
 *  @param {mixedCallback} [cb] - called with the new cache variable
 */
function set_cache(cache_name, cache, cb) {
  this._get_settings((err1, settings) => {
    if (err1) {
      if (cb) return cb(err1);
      else throw err1;
    }

    settings.cache[cache_name] = cache;

    this._set_settings(settings, (err2, settings) => {
      if (err2) {
        if (cb) return cb(err2);
        else throw err2;
      }
      cb(null, cache);
    });
  });
}

/**
 *  sets the credentials object by writing to a settings file
 *  @memberof module:cox-chapi.utils
 *  @deprecated use set_api_key instead
 *  @param {string} api_key - the api_key to use for CloudHealth
 *  @param {objectCallback} [cb] - called with the credentials object
 */
function set_creds(api_key, cb) {
  this._get_settings((err1, settings) => {
    if (err1) {
      if (cb) return cb(err1);
      else throw err1;
    }

    settings.creds = {
      api_key: api_key,
    };

    this._set_settings(settings, (err2, settings) => {
      if (err2) {
        if (cb) return cb(err2);
        else throw err2;
      }
      cb(null, settings.creds);
    });
  });
}

module.exports = utils;