Source: digitalocean/client.js

(function() {
  // External Dependencies
  var url = require('url'),
    request = require('request'),
    Promise = require('bluebird'),
    deepExtend = require('deep-extend');

  // API sections
  var Account = require('./account'),
    Action = require('./action'),
    Domain = require('./domain'),
    Droplet = require('./droplet'),
    LoadBalancer = require('./load_balancer'),
    Volume = require('./volume'),
    FloatingIp = require('./floating_ip'),
    Image = require('./image'),
    Region = require('./region'),
    Size = require('./size'),
    Snapshot = require('./snapshot'),
    Tag = require('./tag');

  // Utilities
  var slice = [].slice,
    util = require('./util'),
    DigitalOceanError = require('./error');

  /**
    * Client resource
    * @class Client
    */
  var Client = (function() {
    /**
     * @typedef ClientOptions
     * @type {object}
     * @property {object} [request] - pass in a specific version of the request library.
     * @property {object} [promise] - pass in a specific version of promise library.
     * @property {object} [requestOptions] - a base set of options provided to Request, overridden by options passed to a specific API method. defaults to empty object.
     * @property {boolean} [decamelizeKeys] - whether to automagically translate query and JSON body keys to use underscore separated names.
     * @property {string} [protocol] - defaults to https.
     * @property {string} [hostname] - defaults to api.digitalocean.com.
     * @property {string} [port] - defaults to protocol default (e.g. with a protocol of https defaults to 443).
     */

    /**
     * Create a new Client instance.
     *
     * @param {string} token - The DigitalOcean token used for authorization.
     * @param {ClientOptions} [options] - An object whose values are used to configure a client instance.
     * @memberof Client
     */
    function Client(token, options) {
      this.token = token;
      this.options = options;

      this.requestOptions = this.options && this.options.requestOptions || {};
      this.request = this.options && this.options.request || request;
      this.promise = this.options && this.options.promise || Promise;
      this.decamelizeKeys = this.options ? this.options.decamelizeKeys : true;

      var version = require('../../package.json').version;
      this.requestDefaults = {
        headers: {
          'User-Agent': 'digitalocean-node/' + version,
          'Content-Type': 'application/json'
        }
      };

      this.version = 'v2';
      this.host = 'api.digitalocean.com';

      this.account = new Account(this);
      this.actions = new Action(this);
      this.domains = new Domain(this);
      this.loadBalancers = new LoadBalancer(this);
      this.volumes = new Volume(this);
      this.floatingIps = new FloatingIp(this);
      this.images = new Image(this);
      this.droplets = new Droplet(this);
      this.regions = new Region(this);
      this.sizes = new Size(this);
      this.snapshots = new Snapshot(this);
      this.tags = new Tag(this);
    }

    /** @private */
    Client.prototype._buildUrl = function(path, urlParams) {
      if (path == null) {
        path = '/';
      }

      if (urlParams == null) {
        urlParams = {};
      }

      var urlFromPath = url.parse(this.version + path);

      if (!!this.decamelizeKeys) {
        urlParams = util.decamelizeKeys(urlParams);
      }

      return url.format({
        protocol: urlFromPath.protocol || this.options && this.options.protocol || "https:",
        auth: urlFromPath.auth || (this.token && this.token.username && this.token.password ? this.token.username + ":" + this.token.password : ''),
        hostname: urlFromPath.hostname || this.options && this.options.hostname || this.host,
        port: urlFromPath.port || this.options && this.options.port,
        pathname: urlFromPath.pathname,
        query: urlParams
      });
    };

    // Returns a function that curries the callback to handle the response from
    // `request`. The returned function has an interface of: `function(err, res, body)`.
    //
    // successStatuses, required, number or array of numbers
    // successRootKeys, required, string or array of strings
    // callback, required, function
    /** @private */
    Client.prototype._buildRequestPromiseHandler = function(requestOptions, successStatuses, successRootKeys, resolve, reject) {
      if (typeof successStatuses === 'number') {
        successStatuses = [successStatuses];
      }

      if (typeof successRootKeys === 'string') {
        successRootKeys = [successRootKeys];
      }

      var self = this;
      var Promise = this.promise;
      return function(err, res, body) {
        if (err) {
          return reject(err);
        }

        // Handle errors on DO's side (5xx level)
        var statusCodeLevel = Math.floor(res.statusCode / 100);
        if (statusCodeLevel === 5) {
          return reject(new DigitalOceanError('Error ' + res.statusCode, res.statusCode, res.headers));
        }

        // Handle improperly returned reponses (e.g. html or something else bizarre)
        if (typeof body === 'string') {
          try {
            body = JSON.parse(body || '{}');
          } catch (jsonParseError) {
            return reject(jsonParseError);
          }
        }

        // Handle validation errors (4xx level)
        if (body.message && statusCodeLevel === 4) {
          return reject(new DigitalOceanError(body.message, res.statusCode, res.headers, body));
        }

        // Handle an unexpcted response code
        if (successStatuses.indexOf(res.statusCode) < 0) {
          return reject(new Error('Unexpected reponse code: ' + res.statusCode));
        }

        // Find the first key from the body object in successRootKeys
        var data = {};
        for (var i = 0; i < successRootKeys.length; i++) {
          var key = successRootKeys[i];
          if (body[key]) {
            data = body[key];
            break;
          }
        }

        // If body.meta.total exists, then it's a paginated response
        var result;
        if (body.meta && body.meta.total) {
          result = new util.ListResponse(self, data, body.meta.total, requestOptions, successStatuses, successRootKeys, Promise);
        } else {
          result = data;
        }

        // Append response data under a special key in the object
        result._digitalOcean = {
          statusCode: res.statusCode,
          body: body,
          headers: res.headers
        };

        return resolve(result);
      };
    };

    /** @private */
    Client.prototype._callbackOrPromise = function(options, overrideOptions, successStatuses, successRootKeys, callback) {
      // Use a hierarchy of options to provide overrides:
      //  1. this.requestDefaults (specified by client)
      //  2. this.requestOptions  (specified by user at client initialization)
      //  3. options              (specfied by client per call type)
      //  4. overrideOptions      (specified by user at call time)
      var requestOptions = deepExtend(this.requestDefaults, this.requestOptions, options, overrideOptions);

      var self = this;
      var deferred = new this.promise(function(resolve, reject) {
        self.request(
          requestOptions,
          self._buildRequestPromiseHandler.apply(self, [requestOptions, successStatuses, successRootKeys, resolve, reject])
        );
      });

      if (callback) {
        // If a callback is provided, translate it to a Promise (to ensure
        // it's called outside of the promise stack, call via timeout)
        deferred
          .then(function(response) {
            setTimeout(function() {
              callback(
                null,
                response,
                response._digitalOcean.headers,
                response._digitalOcean.body
              );
            }, 0);
          })
          .catch(function(error) {
            setTimeout(function() {
              callback(error);
            }, 0);
          });
      }

      return deferred;
    };

    /**
     * Send a HTTP GET request with the specified parameters.
     *
     * @param {string} path - the URL escaped route
     * @param {object} options - an object passed to the request library  See the {@link https://github.com/request/request#requestoptions-callback|request docs} for valid attributes, e.g. a proxy or user agent
     * @param {(number|object)} [page or queryObject] - page number to retrieve or key value pairs of query parameters
     * @param {number} [perPage] - number of result per page to retrieve
     * @param {number|number[]} successStatuses - number or array of numbers corresponding to successful HTTP status codes
     * @param {string|string[]} successRootKeys - string or array of strings corresponding to the root objects containing successful responses
     * @param {requestCallback} [callback] - callback that handles the response
     * @memberof Client
     */
    Client.prototype.get = function() {
      var i;
      var path = arguments[0],
          options = arguments[1],
          params = 4 <= arguments.length ? slice.call(arguments, 2, i = arguments.length - 3) : (i = 2, []),
          successStatuses = arguments[i++],
          successRootKeys = arguments[i++],
          callback = arguments[i++];

      var pageOrQuery = params[0],
          perPage = params[1],
          query = null;
      if ((pageOrQuery != null) && typeof pageOrQuery === 'object') {
        query = pageOrQuery;
      } else {
        query = {};
        if (pageOrQuery != null) {
          query.page = pageOrQuery;
        }
        if (perPage != null) {
          query.per_page = perPage;
        }
      }

      return this._callbackOrPromise(
        {
          uri: this._buildUrl(path, query),
          method: 'GET',
          headers: {
            'Authorization': 'Bearer ' + this.token
          },
          query: query,
          path: path,
        },
        options,
        successStatuses,
        successRootKeys,
        callback
      );
    };

    /** @private */
    Client.prototype._makeRequestWithBody = function(type, path, content, options, successStatuses, successRootKeys, callback) {
      // if `options` isn't passed, shift arguments back by one and set
      // default hash for options
      if (callback == null && (successRootKeys == null || typeof successRootKeys === 'function')) {
        callback = successRootKeys;
        successRootKeys = successStatuses;
        successStatuses = options;
        options = {};
      }

      if (!!this.decamelizeKeys) {
        content = util.decamelizeKeys(content);
      }

      return this._callbackOrPromise(
        {
          uri: this._buildUrl(path, options.query),
          method: type,
          headers: {
            'Authorization': 'Bearer ' + this.token
          },
          body: JSON.stringify(content)
        },
        options,
        successStatuses,
        successRootKeys,
        callback
      );
    };


    /**
     * Send a HTTP POST request with the specified parameters.
     *
     * @param {string} path - the URL escaped route
     * @param {object} content - an object serialized to json
     * @param {object} options - an object passed to the request library  See the {@link https://github.com/request/request#requestoptions-callback|request docs} for valid attributes, e.g. a proxy or user agent
     * @param {number|number[]} successStatuses - number or array of numbers corresponding to successful HTTP status codes
     * @param {string|string[]} successRootKeys - string or array of strings corresponding to the root objects containing successful responses
     * @param {requestCallback} [callback] - callback that handles the response
     * @memberof Client
     */
    Client.prototype.post = function(path, content, options, successStatuses, successRootKeys, callback) {
      return this._makeRequestWithBody('POST', path, content, options, successStatuses, successRootKeys, callback);
    };

    /**
     * Send a HTTP PATCH request with the specified parameters.
     *
     * @param {string} path - the URL escaped route
     * @param {object} content - an object serialized to json
     * @param {object} options - an object passed to the request library  See the {@link https://github.com/request/request#requestoptions-callback|request docs} for valid attributes, e.g. a proxy or user agent
     * @param {number|number[]} successStatuses - number or array of numbers corresponding to successful HTTP status codes
     * @param {string|string[]} successRootKeys - string or array of strings corresponding to the root objects containing successful responses
     * @param {requestCallback} [callback] - callback that handles the response
     * @memberof Client
     */
    Client.prototype.patch = function(path, content, options, successStatuses, successRootKeys, callback) {
      return this._makeRequestWithBody('PATCH', path, content, options, successStatuses, successRootKeys, callback);
    };

    /**
     * Send a HTTP PUT request with the specified parameters.
     *
     * @param {string} path - the URL escaped route
     * @param {object} content - an object serialized to json
     * @param {object} options - an object passed to the request library  See the {@link https://github.com/request/request#requestoptions-callback|request docs} for valid attributes, e.g. a proxy or user agent
     * @param {number|number[]} successStatuses - number or array of numbers corresponding to successful HTTP status codes
     * @param {string|string[]} successRootKeys - string or array of strings corresponding to the root objects containing successful responses
     * @param {requestCallback} [callback] - callback that handles the response
     * @memberof Client
     */
    Client.prototype.put = function(path, content, options, successStatuses, successRootKeys, callback) {
      return this._makeRequestWithBody('PUT', path, content, options, successStatuses, successRootKeys, callback);
    };

    /**
     * Send a HTTP DELETE request with the specified parameters.
     *
     * @param {string} path - the URL escaped route
     * @param {object} content - an object serialized to json
     * @param {object} options - an object passed to the request library  See the {@link https://github.com/request/request#requestoptions-callback|request docs} for valid attributes, e.g. a proxy or user agent
     * @param {number|number[]} successStatuses - number or array of numbers corresponding to successful HTTP status codes
     * @param {string|string[]} successRootKeys - string or array of strings corresponding to the root objects containing successful responses
     * @param {requestCallback} [callback] - callback that handles the response
     * @memberof Client
     */
    Client.prototype.delete = function(path, content, options, successStatuses, successRootKeys, callback) {
      return this._makeRequestWithBody('DELETE', path, content, options, successStatuses, successRootKeys, callback);
    };

    return Client;
  })();

  module.exports = function(token, options) {
    return new Client(token, options);
  };
}).call(this);