cast.js

'use strict'

/**
 * @module Cast
 */

const _ = require('lodash')

/**
 * @name CastFunction
 * @function
 * @param {string} Value to cast
 * @return {*} casted value
 */

const castFunctions = {}

/**
 * Cast to undefined
 * @return {undefined}
 */
castFunctions['undefined'] = () => {
    return undefined
}

/**
 * Cast to null
 * @return {null}
 */
castFunctions['null'] = () => {
    return null
}

/**
 * Cast to number. If is NaN, it throws an error
 * @param {string} value
 * @return {number}
 */
castFunctions['number'] = value => {
    const result = Number(value)
    if (_.isNaN(result)) {
        throw new TypeError(`Unable to cast value to number '${value}'`)
    }
    return result
}

/**
 * Cast to a boolean.
 * @param {string} value - true or false
 * @return {boolean} - true if true. False in all other case.
 */
castFunctions['boolean'] = value => {
    return value === 'true'
}

/**
 * Cast to an array
 * @param {string} value - Should follow the pattern "value1, value2, ..."
 * @return {Array}
 */
castFunctions['array'] = value => {
    return value ? value.replace(/\s/g, '').split(',').map(exports.value) : []
}

/**
 * Cast to as date
 * @param {string} value - today or a date as string
 * @return {string} - A date json formatted
 */
castFunctions['date'] = value => {
    if (value === 'today') {
        return new Date().toJSON().slice(0, 10)
    }

    return new Date(value).toJSON()
}

/**
 * Cast to a string
 * @param {string} value
 * @return {string}
 */
castFunctions['string'] = value => {
    return `${value}`
}

/**
 * Add a new type to cast. This new type can then be used as MyValue((typeName))
 *
 * @example
 * Cast.addType('boolean2', value => value === 'true')
 * //Then it can be used as "true((boolean2))"
 *
 * @param {string} typeName - New type name to add. It will be used in the "(( ))"
 * @param {CastFunction} castFunction
 */
exports.addType = (typeName, castFunction) => {
    if (!_.isFunction(castFunction))
        throw new TypeError(
            `Invalid cast function provided, must be a function (${typeof castFunction})`
        )
    castFunctions[typeName] = castFunction
}

/**
 * Casts a value according to type directives.
 * Supports the following types:
 * - undefined
 * - null
 * - number
 * - boolean
 * - array
 * - date
 * - string
 *
 * @example
 * Cast.value('2((number))')
 * Cast.value('true((boolean))')
 * Cast.value('((null))')
 * Cast.value('raw')
 * // output
 * // > 2
 * // > true
 * // > null
 * // > 'raw'
 *
 * @param {string} value - The value to cast
 * @return {*} The casted value or untouched value if no casting directive found
 */
exports.value = value => {
    if (!_.isString(value)) return value

    const matchResult = value.match(/^(.*)\(\((\w+)\)\)$/)

    if (matchResult) {
        const type = matchResult[2]
        const castFunction = castFunctions[type]
        if (!castFunction) throw new TypeError(`Invalid type provided: ${type} '${value}'`)
        return castFunction(matchResult[1])
    }

    return value
}

/**
 * Casts object all properties.
 *
 * @param {Object} object - The object containing values to cast
 * @return {Object} The object with casted values
 */
exports.object = object => {
    const castedObject = {}
    Object.keys(object).forEach(key => {
        _.set(castedObject, key, exports.value(object[key]))
    })

    return castedObject
}

/**
 * Casts an array of objects.
 *
 * @example
 * Cast.objects([
 *     { username: 'plouc((string))', is_active: 'true((boolean))', age: '25((number))' },
 *     { username: 'john((string))', is_active: 'false((boolean))', age: '32((number))' },
 * ])
 * // output
 * // > [
 * // >    { username: 'plouc', is_active: true, age: 25 },
 * // >    { username: 'john', is_active: false, age: 32 },
 * // > ]
 *
 * @param {Array.<Object>} objects
 */
exports.objects = objects => objects.map(object => exports.object(object))

/**
 * Casts an array of values.
 *
 * @param {Array.<*>} array
 */
exports.array = array => array.map(value => exports.value(value))