Home Manual Reference Source

src/index.js

import is from 'is_js';

/**
 * The exported namespace.
 *
 * All public functions are exported via this namespace.
 *
 * @type {Object}
 * @property {Map<string, Map<string, DummyData>>} allDummyData - The library
 * of dummy data organised by type and period-separated tag path.
 * 
 * This data structure is generated by the {@link refreshDummyData} function.
 */
const muQUnitUtil = {
    /**
     * The library of dummy data organised by type and period-separated tag
     * path.
     *
     * This data structure is generated by the `refreshDummyData()` function.
     *
     * @type {Map<string, Map<string, DummyData>>}
     * @see {@link refreshDummyData}
     */
    allDummyData: {}
};

/**
 * A dummy data definition encapsulating the piece of data itself, a
 * description, and one or more tags.
 *
 * @private
 */
class DummyData{
    
    /**
     * @param {string} desc - a description of the piece of data.
     * @param {string[]} tags - zero or more tags to associate with the data.
     * @param {*} val - the actual piece of data.
     * @param {string} [type] - the data's type.
     * @param {string} [tagPath] - the data's tag path.
     */
    constructor(desc, tags, val, type, tagPath){
        if(!(is.string(desc) && is.not.empty(desc))) throw new TypeError('description must be a non-empty string');
        if(!(is.array(tags) && is.all.string(tags))) throw new TypeError('tags must be an array of strings');
        if(is.not.undefined(type) && is.not.string(type)) throw new TypeError('if p, type must be a string');
        this._description = desc;
        this._tags = [...tags];
        this._value = val;
        this._tagLookup = {};
        this._type = type;
        this._tagPath = tagPath;
        for(const t of this._tags){
            this._tagLookup[t] = true;
        }
    }
    
    /*
     * @type {string}
     */
    get description(){
        return this._description;
    }
    
    /**
     * @type {string[]}
     */
    get tags(){
        return this._tags;
    }
    
    /**
     * @type {*}
     */
    get value(){
        return this._value;
    }
    
    /**
     * @type {string|undefined}
     */
    get type(){
        return this._type;
    }
    
    /**
     * @type {string|undefined}
     */
    get tagPath(){
        return this._tagPath;
    }
    
    /**
     * @return {boolean}
     */
    hasTag(t){
        if(is.not.string(t)) throw new TypeError('tag must be a string');
        return this._tagLookup[t] ? true : false;
    }
}

/**
 * Refresh the dummy data.
 *
 * @alias refreshDummyData
 * @param {...function(): Map<string, Map<string, Array>>} dataGenerators -
 * references to zero or more functions that return additional dummy data
 * beyond the default set. The generators must return a data structure
 * containing three-element arrays indexed by tag path indexed by type. The
 * first element in the arrays must be a textual description of the piece
 * of dummy data, the second a list of additional tags as an array of
 * strings (the tags that make up the tag path should not be included), and
 * the dummy data value. E.g.:
 *
 * ```
 * function muDummyDataGen(){
 *     return {
 *         number: {
 *             'mu.studentNumber': ['a student number', ['integer'], 99999999]
 *         },
 *         string: {
 *             'mu.studentNumber': ['a student number string', ['integer', 'numeric'], '99999999']
 *         }
 *     };
 * }
 * ```
 */
function refreshDummyData(...dataGenerators){
    // The data structure defining the default dummy data - see the relevant
    // page in the manual section of the docs for details.
    const rawData = {
        'boolean': {
            'true': ['true', ['basic'], true],
            'false': ['false', ['falsy'], false]
        },
        'number': {
            'zero': ['the number zero', ['integer', 'falsy'], 0],
            'digit': ['a single-digit number', ['integer'], 7],
            'integer': ['a positive integer', ['basic'], 12345],
            'integer.2digit': ['a 2-digit number', [], 42],
            'integer.3digit': ['a 3-digit number', [], 123],
            'integer.4digit': ['a 4-digit number', [], 1982],
            'uts': ['a numeric Unix Time-stamp', ['datetime', 'integer'], 1529660265],
            'integer.negative': ['a negative integer', [], -12345],
            'float': ['a positive floating point number', [], 3.14],
            'float.negative': ['a negative floating point number', [], -3.14]
        },
        'string': {
            'empty': ['an empty string', ['falsy'], ''],
            'word': ['a single-word string', ['basic'], 'boogers'],
            'line': ['a single-line string', [], 'boogers and snot'],
            'multiline': ['a multi-line string', [''], 'boogers\nsnot\nbogeys'],
            'zero': ['the character 0', ['integer', 'numeric'], '0'],
            'digit': ['a single-digit string', ['integer', 'numeric'], '7'],
            'integer': ['a positive integer string', ['numeric'], '12345'],
            'integer.2digit': ['a 2-digit numeric string', ['numeric'], '42'],
            'integer.3digit': ['a 3-digit numeric string', ['numeric'], '123'],
            'integer.4digit': ['a 4-digit numeric string', ['numeric'], '1982'],
            'uts': ['a Unix Time-stamp string', ['datetime', 'numeric', 'integer'], '1529660265'],
            'iso8601': ['an ISO8601 date & time string', ['datetime'], '2018-06-22T09:37:45z'],
            'rfc2822': ['an RFC2822 date & time string', ['datetime'], 'Fri, 22 Jun 2018 09:37:45 +0000'],
            'jsdate': ['a JavaScript date & time string', ['datetime'], '2018-06-22T09:37:45.000Z'],
            'integer.negative': ['a negative integer string', ['numeric'], '-12345'],
            'float': ['a floating point numeric string', ['numeric'], '3.14'],
            'float.negative': ['a negative floating point numeric string', ['numeric'], '-3.14']
        },
        'array': {
            'empty': ['an empty array', ['object'], []],
            'basic': ['an array of primitives', ['object', 'basic'], [true, 42, 'boogers']]
        },
        'function': {
            'void': ['a void function', ['object', 'basic'], function(){}]
        },
        'error': {
            'Error': ['a generic error', ['object', 'basic'], new Error('a generic error')],
            'TypeError': ['a type error', ['object'], new TypeError('a type error')],
            'RangeError': ['a range error', ['object'], new TypeError('a range error')]
        },
        'object': {
            'null': ['null', ['empty', 'falsy', 'basic'], null],
            'empty': ['empty object', ['plain'], {}],
            'plain': ['a plain object', ['basic'], {a: 'b', c: 42, d: true}],
            'jsdate': ['a date object', ['datetime'], new Date('2018-06-22T09:37:45.000Z')],
            'jsdate.now': ['a date object', ['datetime'], new Date()]
        },
        'other': {
            "undefined": ['undefined', ['empty', 'falsy', 'basic'], undefined]
        }
    };
    const ans = {};
    
    // incorporate the default data
    for(const t of Object.keys(rawData)){
        ans[t] = {};
        for(const tp of Object.keys(rawData[t])){
            ans[t][tp] = new DummyData(
                rawData[t][tp][0],
                [...tp.split('.'), ...rawData[t][tp][1]],
                rawData[t][tp][2],
                t,
                tp
            );
        }
    }
    
    // incporporate the data from the generator functions (if any)
    for(const genFn of dataGenerators){
        try{
            const extraData = genFn();
            if(is.not.object(extraData)) throw new TypeError('generator did not return an object');
            for(const t of Object.keys(extraData)){
                if(is.not.object(extraData[t])) throw new TypeError(`generatedData['${t}'] is not an object`);
                if(is.undefined(ans[t])) ans[t] = {};
                for(const tp of Object.keys(extraData[t])){
                    if(is.not.array(extraData[t][tp])) throw new TypeError(`generatedData['${t}']['${tp}'] is not an array`);
                    if(is.not.string(extraData[t][tp][0])) throw new TypeError(`generatedData['${t}']['${tp}'][0] is not a string`);
                    if(is.not.array(extraData[t][tp][1]) || !is.all.string(extraData[t][tp][1])) throw new TypeError(`generatedData['${t}']['${tp}'][1] is not an array of strings`);
                    ans[t][tp] = new DummyData(
                        extraData[t][tp][0],
                        [...tp.split('.'), ...extraData[t][tp][1]],
                        extraData[t][tp][2],
                        t,
                        tp
                    );
                }
            }
        }catch(err){
            throw new Error(`failed to load additional data from genereator function with error: ${err.message}`);
        }
    }
    
    // store the new data set
    muQUnitUtil.allDummyData = ans;
}
muQUnitUtil.refreshDummyData = refreshDummyData;

/**
 * Returns a single piece of dummy data, or, all the dummy data for a
 * given type, or all the dummy data.
 *
 * To get a single piece of dummy data pass its type and tag path as a
 * single period-separated string, e.g. `'string.word'` for the dummy data
 * with type `string` and tag path `word`, or `'number.integer.negative'`
 * for the dummy data with type `number` and tag path `integer.negative`.
 *
 * To get all the dummy data for a given type pass the type as a string,
 * e.g. `'boolean'` for all dummy boolean data.
 *
 * To get all the dummy data, simply pass `'*'`.
 *
 * When querying all the dummy data both entire sections and specific tags
 * can be excluded, and when querying all the dummy data for a sigle type
 * specific tags can be excluded.
 *
 * @param {string} path - a type, or, a type and tag path as a single
 * period-separated string, or the special value `'*'`.
 * @param {object} [opts] - an optional options object.
 * @param {string[]} [opts.excludeTypes] - a list of types to exclude when
 * requesting all dummy data (`path` is `'*'`).
 * @param {string[]} [opts.excludeTags] - a list of tags to exclude when
 * requesting all dummy data, or the dummy data for a given type.
 * @param {string[]} [opts.excludeDefinitions] - a list of individual data
 * definitions to exclude as period-separated type and tag path strings.
 * @return {DummyData[]|DummyData|undefined} Returns all the dummy data
 * for a given type, or a single piece of dummy data for a type with tag
 * path. If only a type is passed and that type does not exist an empty
 * array is returned, if the path has two or more parts and the type or
 * tag path don't exist, `undefined` is returned.
 * @throws {TypeError}
 */
function dummyData(path, opts){
    if(!(is.string(path) && is.not.empty(path))) throw new TypeError('path must be a non-empty string');
    if(is.not.object(opts)) opts = {};
    const pathParts = path.split('.');
    const reqType = pathParts[0];
    
    // if a single piece of data or a single type is requested, and does not exist, return
    if(reqType !== '*'){
        if(is.not.object(muQUnitUtil.allDummyData[reqType])) return pathParts.length === 1 ? [] : undefined;
    }
    
    // deal with requests for a single piece of data
    if(pathParts.length > 1) return muQUnitUtil.allDummyData[reqType][pathParts.slice(1).join('.')];
    
    // deal with requests for data for one or more types
    
    // figure out what types to process
    const typesToFetch = [];
    if(reqType === '*'){
        const typeSkipLookup = {};
        if(is.array(opts.excludeTypes)){
            for(const t of opts.excludeTypes) typeSkipLookup[t] = true;
        }
        for(const t of Object.keys(muQUnitUtil.allDummyData)){
            if(!typeSkipLookup[t]) typesToFetch.push(t);
        }
    }else{
        typesToFetch.push(reqType);
    }
    
    // figure out which individual definitions to skip
    const defSkipLookup = {};
    if(is.array(opts.excludeDefinitions)){
        for(const dp of opts.excludeDefinitions) defSkipLookup[dp] = true;
    }
    
    // process all the requested types
    const ans = [];
    const doCheckTags = is.array(opts.excludeTags);
    for(const t of typesToFetch){
        processTypeDummyData:
        for(const tp of Object.keys(muQUnitUtil.allDummyData[t])){
            if(doCheckTags){
                for(const et of opts.excludeTags){
                    if(muQUnitUtil.allDummyData[t][tp].hasTag(et)) continue processTypeDummyData;
                }
            }
            if(!defSkipLookup[`${t}.${tp}`]) ans.push(muQUnitUtil.allDummyData[t][tp]);
        }
    }
    return ans;
}
muQUnitUtil.dummyData = dummyData;

/**
 * A function to return all dummy data except those for the given
 * types and those matching the given tags tags.
 *
 * This is a shortcut for:
 * 
 * ```
 * .dummyData(
 *     '*',
 *     {
 *         excludeTypes: arguments[0],
 *         excludeTags: arguments[1],
 *         excludeDefinitions: arguments[2]
 *     }
 * )
 * ```
 *
 * @param {string[]} [excludeTypes]
 * @param {string[]} [excludeTags]
 * @param {string[]} [excludeDefinitions]
 * @return {DummyData[]}
 */
function dummyDataExcept(excludeTypes, excludeTags, excludeDefinitions){
    if(is.not.array(excludeTypes)) excludeTypes = [];
    if(is.not.array(excludeTags)) excludeTags = [];
    if(is.not.array(excludeDefinitions)) excludeDefinitions = [];
    return dummyData('*', {excludeTypes, excludeTags, excludeDefinitions});
}
muQUnitUtil.dummyDataExcept = dummyDataExcept;

/**
 * A function to return all basic dummy data, i.e. all dummy data tagged
 * `basic`.
 *
 * This is a shortcut for `dummyDataWithAnyTag('basic')`.
 *
 * @return {DummyData[]}
 */
function dummyBasicData(){
    return dummyDataWithAnyTag('basic');
}
muQUnitUtil.dummyBasicData = dummyBasicData;

/**
 * A function to return all basic dummy data that's not an object, i.e. all
 * dummy data tagged `basic` that does not have either the type or tag
 * `object`.
 *
 * @return {DummyData[]}
 */
function dummyBasicPrimitives(){
    const ans = [];
    for(const dd of dummyBasicData()){
        if(dd.type != 'object' && !dd.hasTag('object')) ans.push(dd);
    }
    return ans;
}
muQUnitUtil.dummyBasicPrimitives = dummyBasicPrimitives;

/**
 * A function to return all basic dummy data except those for zero or more
 * given types.
 *
 * @param {...string} excludeTypes
 * @return {DummyData[]}
 */
function dummyBasicDataExcept(...excludeTypes){
    const excludeLookup = {};
    for(const et of excludeTypes) excludeLookup[et] = true;
    const ans = [];
    for(const dd of dummyBasicData()){
        if(!excludeLookup[dd.type]){
            ans.push(dd);
        }
    }
    return ans;
}
muQUnitUtil.dummyBasicDataExcept = dummyBasicDataExcept;

/**
 * A function to return all basic dummy primitives except those for zero or
 * more given types.
 *
 * @param {...string} excludeTypes
 * @return {DummyData[]}
 */
function dummyBasicPrimitivesExcept(...excludeTypes){
    const excludeLookup = {};
    for(const et of excludeTypes) excludeLookup[et] = true;
    const ans = [];
    for(const dd of dummyBasicPrimitives()){
        if(!excludeLookup[dd.type]){
            ans.push(dd);
        }
    }
    return ans;
}
muQUnitUtil.dummyBasicPrimitivesExcept = dummyBasicPrimitivesExcept;

/**
 * Returns the dummy data of one or more types.
 * 
 * @param {...string} typeList
 * @return {DummyData[]}
 * @throws {TypeError}
 */
function dummyDataByType(...typeList){
    if(!is.all.string(typeList)) throw new TypeError('all specified types must be strings');
    const ans = [];
    for(const t of typeList){
        if(is.object(muQUnitUtil.allDummyData[t])) ans.push(...Object.values(muQUnitUtil.allDummyData[t]));
    }
    return ans;
}
muQUnitUtil.dummyDataByType = dummyDataByType;

/**
 * Returns the dummy data matching **any** of the given tags.
 *
 * @param {...string} tagList
 * @return {DummyData[]}
 * @throws {TypeError}
 */
function dummyDataWithAnyTag(...tagList){
    if(!is.all.string(tagList)) throw new TypeError('all specified tags must be strings');
    const ans = [];
    for(const td of Object.values(muQUnitUtil.allDummyData)){
        for(const dd of Object.values(td)){
            // test if any requested tag is present
            let anyPresent = false;
            for(const t of tagList){
                if(dd.hasTag(t)){
                    anyPresent = true;
                    break;
                }
            }
            if(anyPresent) ans.push(dd);
        }
        
    }
    return ans;
}
muQUnitUtil.dummyDataWithAnyTag = dummyDataWithAnyTag;

/**
 * Returns the dummy data matching **all** of the given tags.
 * 
 * @param {...string} tagList
 * @return {DummyData[]}
 * @throws {TypeError}
 */
function dummyDataWithAllTags(...tagList){
    if(!is.all.string(tagList)) throw new TypeError('all specified tags must be strings');
    const ans = [];
    for(const td of Object.values(muQUnitUtil.allDummyData)){
        for(const dd of Object.values(td)){
            // make sure every tag is present
            let allPresent = true;
            for(const t of tagList){
                if(!dd.hasTag(t)){
                    allPresent = false;
                    break;
                }
            }
            if(allPresent) ans.push(dd);
        }
    }
    return ans;
}
muQUnitUtil.dummyDataWithAllTags = dummyDataWithAllTags;

// initialise the dummy data
refreshDummyData();

export default muQUnitUtil;