patterns | extend prototype class | test enforcing an interface with prototype | Search

This code provides a collection of functions (typeErrorTemplate, standardCompare, arrayCompare, objectCompare, and interface) for comparing and validating values against expected structures, with the ability to handle type mismatches and loose comparisons.

The code includes several functions for comparing and validating values, including standardCompare, arrayCompare, and objectCompare, which compare arrays and objects against expected structures. The typeErrorTemplate function is used to create custom error messages for type mismatches, and the interface function provides a general-purpose comparison function.

Run example

npm run import -- "enforcing an interface"

enforcing an interface


// newer ES6 syntax

function typeErrorTemplate(e, k, t, i, p) {
    if(k) {
        k = ' ' + k;
    }
    if(p) {
        p = ' of type ' + p;
    }
    throw new Error(`type mis-match${k || ''}: "${t}" is not "${i}"${p || ''}`, e)
}

function standardCompare(type, expected) {
    if(type === expected) return true;
    if(!type || !expected || !(expected.isPrototypeOf(type))) {
        return false
    }
    return true;
}

function arrayCompare(compare, specification, loosey) {
    var match = specification.map(i => {
        try {
            return interface(compare, i, loosey);
        } catch (e) {
            return e;
        }
    }).filter(s => !s || s.constructor !== Error);
    if(match.length > 0) {
        return match[0];
    }
    typeErrorTemplate(void 0,
                      void 0,
                      typeof compare,
                      specification.map(s => typeof s));
}

function objectCompare(compare, specification, loosey) {
    var match = Object.keys(specification).reduce((map, k) => {
        try {
            var m = interface(compare[k], specification[k], loosey);
            if(k === 'kernel_config') {
            }
            if(typeof m !== 'undefined') {
                map[k] = m;
            }
        } catch (e) {
            typeErrorTemplate(e,
                              k,
                              typeof compare[k],
                              specification[k],
                              specification[k].constructor)
        }
        return map;
    }, {});
    return match;
}

// loosey means no exceptions are thrown and undefined type is assumed
function interface(compare, specification, loosey) {
    var type = compare === void 0 || compare === null
        ? compare
        : Object.getPrototypeOf(compare);
    var expected = specification === void 0 || specification === null
        ? specification
        : Object.getPrototypeOf(specification);
    switch(expected) {
        case Array.prototype:
            // if it is an empty array to the actual compare on the object,
            //   if it is not an empty array compare to each type in the array
            if(specification.length > 0) {
                return arrayCompare(compare, specification, loosey)
            }
        case Object.prototype:
            // compare properties on object
            if(Object.keys(specification).length > 0
               && (typeof compare === 'object' || typeof compare === 'function')) {
               return objectCompare(compare, specification, loosey)
            }
        default:
            if(standardCompare(type, expected)
               // if loosey is not explicitly set to false,
               //   return void 0 (undefined) instead of error
               //   only if compare it undefined,
               //   still error on other wrong types
               || (loosey !== false && compare === void 0)) {
                return compare;
            }
            typeErrorTemplate(void 0,
                              void 0,
                              typeof compare,
                              typeof specification,
                              expected)
    }
}

module.exports = interface;

if(typeof $ !== 'undefined') {
    var jsonInterface = {
        display_name: '',
        argv: [], // not optional
        language: '', // not optional
        metadata: [void 0, {}], // optional
        env: [void 0, {}], // TODO dictionary descriptor types?
        interrupt_mode: [void 0, '']
    }

    console.log(interface({
        display_name: 'Node JS',
        argv: [],
        language: '',
        some_other_stuff: true
    }, jsonInterface))
    
    // expected output {"display_name":"Node JS","argv":[],"language":""}
}

What the code could have been:

/**
 * Type error template function.
 * Throws an error if the type of a variable does not match the expected type.
 *
 * @param {Error} e The error to throw.
 * @param {string} k The key or name of the variable (optional).
 * @param {string} t The type of the variable.
 * @param {string} i The expected type.
 * @param {string} p The type of the expected type (optional).
 */
function typeErrorTemplate(e, k, t, i, p) {
    if (k) {
        k = ` ${k}`;
    }
    if (p) {
        p = ` of type ${p}`;
    }
    throw new Error(`Type mis-match${k || ''}: "${t}" is not "${i}"${p || ''}`, e);
}

/**
 * Checks if two types are equal or if the expected type is a parent of the actual type.
 *
 * @param {*} type The actual type.
 * @param {*} expected The expected type.
 * @returns {boolean} True if the types are equal or if the expected type is a parent of the actual type, false otherwise.
 */
function standardCompare(type, expected) {
    return type === expected || expected.isPrototypeOf(type);
}

/**
 * Compares an array to an array specification.
 * Throws an error if the array does not match the specification.
 *
 * @param {*} compare The array to compare.
 * @param {array} specification The array specification.
 * @param {boolean} loosey Whether to throw an error or return undefined if the types do not match.
 * @returns {*} The compared array or an error.
 */
function arrayCompare(compare, specification, loosey) {
    const match = specification.map((i, index) => {
        try {
            return interface(compare[index], i, loosey);
        } catch (e) {
            return e;
        }
    }).filter(s =>!s ||!(s instanceof Error));

    if (match.length > 0) {
        return match[0];
    }

    typeErrorTemplate(new Error('Type mis-match'), void 0, typeof compare, specification.map(s => typeof s));
}

/**
 * Compares an object to an object specification.
 * Throws an error if the object does not match the specification.
 *
 * @param {*} compare The object to compare.
 * @param {object} specification The object specification.
 * @param {boolean} loosey Whether to throw an error or return undefined if the types do not match.
 * @returns {*} The compared object or an error.
 */
function objectCompare(compare, specification, loosey) {
    const match = Object.keys(specification).reduce((map, key) => {
        try {
            const m = interface(compare[key], specification[key], loosey);
            if (key === 'kernel_config') {
                // No-op for kernel_config
            }
            if (typeof m!== 'undefined') {
                map[key] = m;
            }
        } catch (e) {
            typeErrorTemplate(e, key, typeof compare[key], specification[key]);
        }
        return map;
    }, {});

    return match;
}

/**
 * Compares two values using the interface function.
 *
 * @param {*} compare The value to compare.
 * @param {*} specification The value specification.
 * @param {boolean} loosey Whether to throw an error or return undefined if the types do not match.
 * @returns {*} The compared value or an error.
 */
function interface(compare, specification, loosey = false) {
    let type, expected;

    if (compare === void 0 || compare === null) {
        type = compare;
    } else {
        type = Object.getPrototypeOf(compare);
    }

    if (specification === void 0 || specification === null) {
        expected = specification;
    } else {
        expected = Object.getPrototypeOf(specification);
    }

    switch (expected) {
        case Array.prototype:
            if (specification.length > 0) {
                return arrayCompare(compare, specification, loosey);
            }
        case Object.prototype:
            if (Object.keys(specification).length > 0 && (typeof compare === 'object' || typeof compare === 'function')) {
                return objectCompare(compare, specification, loosey);
            }
        default:
            if (standardCompare(type, expected) || (loosey && compare === void 0)) {
                return compare;
            }

            typeErrorTemplate(new Error('Type mis-match'), void 0, typeof compare, typeof specification, expected);
    }
}

module.exports = interface;

if (typeof $!== 'undefined') {
    const jsonInterface = {
        display_name: '',
        argv: [], // not optional
        language: '', // not optional
        metadata: [void 0, {}], // optional
        env: [void 0, {}], // TODO dictionary descriptor types?
        interrupt_mode: [void 0, '']
    };

    console.log(interface({
        display_name: 'Node JS',
        argv: [],
        language: '',
        some_other_stuff: true
    }, jsonInterface));
}

Code Breakdown

Functions

typeErrorTemplate(e, k, t, i, p)

standardCompare(type, expected)

arrayCompare(compare, specification, loosey)

objectCompare(compare, specification, loosey)

interface(compare, specification, loosey)

Notes