hybridables/redolent

View on GitHub
index.js

Summary

Maintainability
A
1 hr
Test Coverage
/*!
 * redolent <https://github.com/tunnckoCore/redolent>
 *
 * Copyright (c) Charlike Mike Reagent <@tunnckoCore> (https://i.am.charlike.online)
 * Released under the MIT license.
 */

'use strict'

import arrify from 'arrify'
import sliced from 'sliced'
import extend from 'extend-shallow'
import register from 'native-or-another/register'
import Promize from 'native-or-another'
import isAsync from 'is-async-function'

/**
 * > Will try to promisify `fn` with native Promise,
 * otherwise you can give different promise module
 * to `opts.Promise`, for example [pinkie][] or [bluebird][].
 * If `fn` [is-async-function][] it will be passed with `done` callback
 * as last argument - always concatenated with the other provided args
 * through `opts.args`.
 *
 * **Note:** Uses [native-or-another][] for detection, so it will always will use the
 * native Promise, otherwise will try to load some of the common promise libraries
 * and as last resort if can't find one of them installed, then throws an Error!
 *
 * **Example**
 *
 * ```js
 * const fs = require('fs')
 * const request = require('request')
 * const redolent = require('redolent')
 *
 * redolent(fs.readFile)('package.json', 'utf-8').then(data => {
 *   console.log(JSON.parse(data).name)
 * })
 *
 * // handles multiple arguments by default
 * redolent(request)('http://www.tunnckocore.tk/').then(result => {
 *   const [httpResponse, body] = result
 * })
 *
 * // `a` and `b` arguments comes from `opts.args`
 * // `c` and `d` comes from the call of the promisified function
 * const fn = redolent((a, b, c, d, done) => {
 *   console.log(typeof done) // => 'function'
 *   done(null, a + b + c + d)
 * }, {
 *   args: [1, 2]
 * })
 *
 * fn(3, 5).then((res) => {
 *   console.log(res) // => 11
 * })
 * ```
 *
 * @name   redolent
 * @param  {Function} `<fn>` a function to be promisified
 * @param  {Object} `[opts]` optional options, also passed to [native-or-another][]
 * @param  {Array} `[opts.args]` additional arguments to be passed to `fn`,
 *                               all args from `opts.args` and these that are
 *                               passed to promisifed function are concatenated
 * @param  {Object} `[opts.context]` what context to be applied to `fn`,
 *                                   by default it is smart enough and applies
 *                                   the `this` context of redolent call or the call
 *                                   of the promisified function
 * @param  {Function} `[opts.Promise]` custom Promise constructor for versions `< v0.12`,
 *                                   like [bluebird][] for example, by default
 *                                   it **always** uses the native Promise in newer
 *                                   node versions
 * @param  {Boolean} `[opts.global]` defaults to `true`, pass false if you don't
 *                                   want to attach/add/register the given promise
 *                                   to the `global` scope, when node `< v0.12`
 * @return {Function} promisified function
 * @throws {TypeError} If `fn` is not a function
 * @throws {TypeError} If no promise is found
 * @api public
 */

export default function redolent (fn, opts) {
  if (typeof fn !== 'function') {
    throw new TypeError('redolent: expect `fn` to be a function')
  }

  opts = extend({ context: this, Promise: Promize }, opts)
  opts.Promise = register(opts)

  // we can't test that here, because some
  // of our devDeps has some Promise library,
  // so it's loaded by `native-or-another` automatically
  /* istanbul ignore next */
  if (typeof opts.Promise !== 'function') {
    var msg = 'no native Promise support nor other promise were found'
    throw new TypeError('redolent: ' + msg)
  }

  return function () {
    opts.context = this || opts.context
    opts.args = arrify(opts.args).concat(sliced(arguments))

    var promise = new opts.Promise(function (resolve, reject) {
      var called = false

      function done (er, res) {
        called = true
        if (er) {
          return reject(er)
        }
        if (arguments.length > 2) {
          res = sliced(arguments, 1)
        }
        return resolve(res)
      }

      var isAsyncFn = isAsync(fn)

      opts.args = isAsyncFn ? opts.args.concat(done) : opts.args
      var syncResult = fn.apply(opts.context, opts.args)

      var xPromise = isPromise(syncResult, opts.Promise)
      var hasPromiseReturn = isAsyncFn && !called && xPromise

      if ((!isAsyncFn && !called) || hasPromiseReturn) {
        resolve(syncResult)
        return
      }
      if (isAsyncFn && !xPromise && syncResult !== undefined) {
        var msg = 'Asynchronous functions can only return a Promise or invoke a callback'
        reject(new Error('redolent: ' + msg))
      }
    })

    return normalize(promise, opts.Promise)
  }
}

function normalize (promise, Ctor) {
  promise.___nativePromise = Boolean(Ctor.___nativePromise)
  promise.___customPromise = Boolean(Ctor.___customPromise)
  return promise
}

/* istanbul ignore next */
function isPromise (val, Promize) {
  return val instanceof Promize || (
    val !== null &&
    typeof val === 'object' &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}