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;