Source: sources/cores/cores.js

/**
 * @author [Tristan Valcke]{@link https://github.com/Itee}
 * @license [BSD-3-Clause]{@link https://opensource.org/licenses/BSD-3-Clause}
 *
 * @module sources/cores/cores
 * @description Export the Validator singleton instance that allow to validate complex data structure
 * @example
 *
 const validator = Itee.Validators.Validator

 // Using unique function for One registered type
 // Usefull when a simple data structure is used multiple times
 validator.add( 'ColorType', color => {

                const r = color.r
                if ( color.r === undefined || Itee.Validators.isNotNumber( r ) ) {
                    return false
                }

                const g = color.g
                if ( color.g === undefined || Itee.Validators.isNotNumber( g ) ) {
                    return false
                }

                const b = color.b
                if ( color.b === undefined || Itee.Validators.isNotNumber( b ) ) {
                    return false
                }

                return true
            } )

 // Using schema composition
 // Usefull for design validation schema faster and based on previous declared validation types
 validator.add( 'Range_0_255', ( value ) => {

                if ( Itee.Validators.isNotNumber( value ) ) {
                    return false
                }

                return !(value <= 0 || value > 255)

            } )

 validator.add( 'ColorSchema', {
                r: {
                    required: true,
                    type:     'Range_0_255'
                },
                g: {
                    required: true,
                    type:     'Range_0_255'
                },
                b: {
                    required: true,
                    type:     'Range_0_255'
                }
            } )

 validator.add( 'ColorStructure', {
                color_from_type: {
                    type: 'ColorType'
                },
                col_from_schema: {
                    type: 'ColorSchema'
                },
                col_from_fn:     {
                    // Inner function
	                // Usefull for specific validation requirement that cannot match other previous validation schema or type
                    fn: function ColorValidator ( color ) {

                        const r = color.r
                        if ( color.r === undefined || Itee.Validators.isNotNumber( r ) ) {
                            return false
                        }

                        const g = color.g
                        if ( color.g === undefined || Itee.Validators.isNotNumber( g ) ) {
                            return false
                        }

                        const b = color.b
                        if ( color.b === undefined || Itee.Validators.isNotNumber( b ) ) {
                            return false
                        }

                        return true
                    }
                }
            } )

 // The data to validate
 const colorStruct = {
                color_from_type: {
                    r: 0,
                    g: 1,
                    b: 2
                },
                col_from_schema: {
                    r: 10,
                    g: 20,
                    b: 30
                },
                col_from_fn:     {
                    r: 0,
                    g: 127,
                    b: 255
                }
            }

 // Execute

 try {

    if ( validator.check( colorStruct, 'ColorStructure' ) ) {
        alert( 'ColorStructure is valid !' )
    } else {
        alert( validator.errors )
    }

} catch ( err ) {
    alert( err )
}
 *
 */

import { isArray }   from './arrays/_arrays'
import { isBoolean } from './booleans/_booleans'
import {
    isFunction,
    isNotFunction
}                    from './functions/_functions'
import {
    isFloat,
    isInteger,
    isNumber
}                    from './numbers/_numbers'
import {
    isNotObject,
    isObject
}                    from './objects/_objects'
import {
    isNotString,
    isString
}                    from './strings/_strings'
import { isSymbol }  from './symbols/_symbols'
import {
    isDefined,
    isNotDefined
}                    from './voids/_voids'

class Validator {

    constructor () {

        /**
         * The validators store, by default it contains validators for Boolean, Number, Integer, Float, Array, String, Object, Symbol and Function
         *
         * @type {object}
         */
        this.validators = {
            Boolean:  isBoolean,
            Number:   isNumber,
            Integer:  isInteger,
            Float:    isFloat,
            Array:    isArray,
            String:   isString,
            Object:   isObject,
            Symbol:   isSymbol,
            Function: isFunction
        }

        /**
         * The list of errors occured during the check
         *
         * @type {Array.<string>}
         */
        this.errors = []

    }

    /**
     * Add a new validator schema to the validator instance
     *
     * @param type {string} - A string that represent the type of data to validate
     * @param validator {(function|object)} - A function or validation schema that represent the type of data to validate
     */
    add ( type, validator ) {

        if ( isNotString( type ) ) { throw new TypeError( `Validator: Expect type to be a string` ) }
        if ( isNotFunction( validator ) && isNotObject( validator ) ) { throw new TypeError( `Validator: Expect validator to be an object or a function` ) }
        if ( isDefined( this.validators[ type ] ) ) { throw new TypeError( `Validator: a validator is already defined for type '${ type }'` ) }

        this.validators[ type ] = validator

    }

    /**
     * To remove a registered type
     *
     * @param type {string} - The type to remove
     */
    remove ( type ) {

        delete this.validators[ type ]

    }

    /**
     * Allow to known the available types store in current validator instance
     *
     * @return {Array.<string>} - The list of registered type
     */
    getAvalaibleTypes () {

        const availablesTypes = []

        for ( let key in this.validators ) {
            availablesTypes.push( key )
        }

        return availablesTypes

    }

    /**
     * Will perform a deep structural comparison between the given data and the validation schema of the given type
     *
     * @param data {*} - The data to validate
     * @param type {string} - The type of the validation schema to apply
     * @param breakOnError {boolean} - Return on first validation error ( true by default )
     * @return {boolean} - Return true is the data is validated, false otherwise
     */
    check ( data, type, breakOnError = true ) {

        const validator = this.validators[ type ]
        if ( isNotDefined( validator ) ) {
            throw new TypeError( `Validator: Unable to find schema validation of type '${ type }'` )
        }

        let result = true
        if ( isFunction( validator ) ) {

            result = validator( data )

        } else if ( isObject( validator ) ) {

            let subResult = true
            for ( let key in validator ) {

                const subValidator = validator[ key ]
                if ( isNotDefined( subValidator ) ) {
                    throw new TypeError( `Validator: Missing validator for key '${ key }' of type '${ type }'` )
                }

                const value      = data[ key ]
                const isRequired = subValidator.required
                if ( isNotDefined( value ) ) {
                    if ( isRequired ) {
                        subResult = false
                    } else {
                        continue
                    }
                }

                // In case of overriden validation function check it first
                let validatorFunction = subValidator.fn
                if ( isDefined( validatorFunction ) ) {

                    if ( isNotFunction( validatorFunction ) ) {
                        throw new TypeError( `Validator: Invalid validation function for '${ key }' with type '${ type }'` )
                    }

                    subResult = validatorFunction( value )

                } else {

                    subResult = this.check( value, subValidator.type, breakOnError )

                }

                if ( subResult === false ) {

                    this.errors.push( `Validator: Invalid property '${ key }' of type '${ subValidator.type }' with value '${ value }' in object of type '${ type }'` )
                    result = false
                    if ( breakOnError ) {
                        break
                    }

                }

            }

        } else {

            throw new TypeError( `Validator: Unknown validator of type '${ type }'` )

        }

        return result

    }

}

/**
 * The singleton instance
 *
 * @type {Validator}
 */
let validatorInstance = undefined
if ( isNotDefined( validatorInstance ) ) {
    validatorInstance = new Validator()
}

export { validatorInstance as Validator }