Source: lib/function-utils.js

'use strict'

const _ = require('lodash'),
    expect = require('chai').expect,
    errors = require('./errors')

// const eyes = require('eyes'), p = eyes.inspect.bind(eyes)

function oneFunctionArgWithLength(length, func) {
    expect(func).to.be.a('function')
    const fname = func.name
    expect(func).to.have.length.of.at.least(length, `Wrapped function ${fname} must accept at least ${length} arguments`)
    return fname
}

function argumentsWithCallback(arraylike, length, fname) {
    let args = _.toArray(arraylike)
    expect(args).to.have.length.of.at.least(length, `Function ${fname} called with ${args.length} arguments requires at least ${length}`)
    expect(args[args.length - 1]).to.be.a('function', `Function ${fname} cannot be called without a callback as its final argument`)
    return args
}

/**
 * Standard `function(error, result) { ... }` callbacks that get automatically wrapped so that either `error` or `result` must be `null`.
 * - **Write your callbacks like you normally would they will be scrubbed automatically.** Code in scrubbed callbacks does not have to worry about cases in which both arguments passed to the callback will be non-`null` at least one will always be `null` (callbacks that expect `null` as a result will still work).
 * @callback ScrubbedCallback
 * @memberof module:classeur-api-client
 * @param {?(module:classeur-api-client.ServerError|module:classeur-api-client.ClientError)} error - An Error or error String, if an error occurred executing the function to which this callback was supplied, or `null` if the function was successful.
 * - `error` will be a [ServerError]{@link module:classeur-api-client.ServerError} or [ClientError]{@link module:classeur-api-client.ClientError}, or `null` on success.
 * - All functions in this module will supply a [ServerError]{@link module:classeur-api-client.ServerError} or [ClientError]{@link module:classeur-api-client.ClientError} (both of which are instances of [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)) to their callback there is no need to check for stringy errors.
 * - `error` will always be `null` (not `undefined` or another falsy value) if no error occurred.
 * @param {?*} result - Result of the function to which this callback was supplied, or `null` if the function was unsuccessful.
 * - For type information of `result`, see the documentation for the function to which the callback was passed.
 * - `result` will be `null` even if the function was _partially_ successful, and was supposed to return an Array partial result arrays are not supported.
 */

// The function that does the callback scrubbing.
module.exports.scrubArrayCallback = function(cb, wantarray, length) {
    return function(err, res) {
        if ( ! err ) {
            expect(res).to.be.instanceof(Array, 'Result is not an Array')
            expect(res.length).to.equal(length, 'Result array size mismatch')
            if ( ! wantarray ) {
                res = res[0]
            } else {
                res = _.flatten(res)
            }
        }
        cb(err || null, err ? null : res)
    }
}

// Simulates function(a, b, c, ...args, cb), or function(a, b, c, [args], cb).
module.exports.restOrArrayAndCallback = function(wrapped, thisArg) {
    const fname = oneFunctionArgWithLength(2, wrapped)
    return function() {
        let args = argumentsWithCallback(arguments, 2, fname)
        // Assumes the last two are an array and a callback.
        const statics = args.splice(0, wrapped.length - 2),
            cb = args.pop()

        if ( ! ( args.length === 1 && _.isArrayLike(args[0]) ) ) {
            args = [args]
        }

        _.bind(wrapped, thisArg)(...statics.concat(args, cb))
    }
}

// Asserts that a function only takes a single, non-array argument and a
// callback in addition to its other arguments.
module.exports.singleElementAndCallback = function(wrapped, thisArg) {
    const fname = oneFunctionArgWithLength(2, wrapped)
    return function() {
        let args = argumentsWithCallback(arguments, 2, fname)
        const idx = args.length - 2
        expect(args[idx]).not.to.be.instanceof(Array, `Function ${fname} cannot be called with an array as the 2nd-to-last argument`)

        args[idx] = [args[idx]]
        _.bind(wrapped, thisArg)(...args)
    }
}