felicity
Version:
Javascript object instantiation from Joi schema
868 lines (662 loc) • 28.1 kB
JavaScript
'use strict';
const Hoek = require('@hapi/hoek');
const Joi = require('./joi');
const RandExp = require('randexp');
const Uuid = require('uuid');
const Moment = require('moment');
const Helpers = require('./helpers');
const internals = {
JoiArrayProto: Reflect.getPrototypeOf(Joi.array()),
JoiBoolProto: Reflect.getPrototypeOf(Joi.boolean()),
JoiFuncProto: Reflect.getPrototypeOf(Joi.func()),
JoiNumberProto: Reflect.getPrototypeOf(Joi.number())
};
internals.getType = function (schema) {
const schemaDescription = schema.describe();
let exampleType = schemaDescription.type;
if (Examples[exampleType] === undefined) {
exampleType = 'any';
if (schema.append !== undefined) {
exampleType = 'func';
}
else if (schema.unique !== undefined) {
exampleType = 'array';
}
else if (schema.truthy !== undefined) {
exampleType = 'boolean';
}
else if (schema.greater !== undefined) {
exampleType = 'number';
}
}
return exampleType;
};
class Any {
constructor(schema, options) {
this._schema = schema;
this._options = options && options.config;
}
generate() {
const schemaDescription = this._schema.describe();
if (Hoek.reach(schemaDescription, 'allow')) {
if (Hoek.reach(this._options, 'ignoreValids') !== true) {
return Helpers.pickRandomFromArray(schemaDescription.allow);
}
}
const flagDefault = Hoek.reach(schemaDescription, 'flags.default');
if (Hoek.reach(this, '_options.ignoreDefaults') !== true && Hoek.reach(schemaDescription, 'flags.default') !== undefined) {
if (typeof flagDefault === 'function') {
return flagDefault();
}
return this._getDefaults();
}
if (Hoek.reach(schemaDescription, 'examples')) {
return Helpers.pickRandomFromArray(schemaDescription.examples.flat(Infinity));
}
const rules = this._buildRules();
return this._generate(rules);
}
_generate(rules) {
return Math.random().toString(36).substr(2);
}
_buildRules() {
const rules = this._schema.describe().rules || [];
const options = {};
rules.forEach((rule) => {
options[rule.name] = rule.args === undefined || rule.args === null ? true : rule.args;
});
return options;
}
_getDefaults() {
return Helpers.getDefault(this._schema.describe());
}
}
class StringExample extends Any {
_generate(rules) {
const specials = {
hostname: () => {
const randexp = new RandExp(/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/);
randexp.max = 5;
return randexp.gen();
},
token: () => {
return new RandExp(/[a-zA-Z0-9_]+/).gen();
},
hex: () => {
return new RandExp(/^[a-f0-9]+$/i).gen();
},
creditCard: () => {
let creditCardNumber = '';
let sum = 0;
while (creditCardNumber.length < 11) {
const randomInt = Math.floor(Math.random() * 10);
creditCardNumber = randomInt.toString() + creditCardNumber;
if (creditCardNumber.length % 2 !== 0) {
const doubleInt = randomInt * 2;
const digit = doubleInt > 9 ?
doubleInt - 9 :
doubleInt;
sum = sum + digit;
}
else {
sum = sum + randomInt;
}
}
const guardDigit = (sum * 9) % 10;
return creditCardNumber + guardDigit.toFixed();
},
pattern: () => {
const pattern = (rules.pattern.options && rules.pattern.options.invert) ? /[a-f]{3}/ : rules.pattern.regex;
const result = new RandExp(pattern).gen();
return result.replace(/^(\s|\/)+|(\s|\/)+$/g, '');
},
guid: () => {
return Uuid.v4();
},
ip: () => {
const possibleResults = [];
let isIPv4 = true;
let isIPv6 = false;
let isCIDR = true;
if (rules.ip.options.version) {
isIPv4 = rules.ip.options.version.indexOf('ipv4') > -1;
isIPv6 = rules.ip.options.version.indexOf('ipv6') > -1;
}
if (rules.ip.options.cidr === 'forbidden') {
isCIDR = false;
}
if (isIPv4) {
possibleResults.push('224.109.242.85');
if (isCIDR) {
possibleResults.push('224.109.242.85/24');
}
}
if (isIPv6) {
possibleResults.push('8194:426e:9389:5963:1a5:9c75:31ae:ccbb');
if (isCIDR) {
// TODO : this needs to be replaced with a IPv6 CIDR. I think Joi has issues validating a real CIRD atm.
possibleResults.push('8194:426e:9389:5963:1a5:9c75:31ae:ccbb');
}
}
return possibleResults[Math.floor(Math.random() * (possibleResults.length))];
},
email: () => {
const domains = [
'email.com',
'gmail.com',
'example.com',
'domain.io',
'email.net'
];
return Math.random().toString(36).substr(2) + '@' + Helpers.pickRandomFromArray(domains);
},
isoDate: () => {
return (new Date()).toISOString();
},
uri: () => {
return `${['http', 'https', 'ftp'][Math.floor(Math.random() * 3)]}://www.${Math.random().toString(36).substr(2)}.${['com', 'net', 'gov'][Math.floor(Math.random() * 3)]}`;
}
};
const specialRules = Hoek.intersect(Object.keys(specials), Object.keys(rules));
let stringGen = () => Math.random().toString(36).substr(2);
if (specialRules.length > 0) {
if (specialRules[0] === 'hex') {
stringGen = specials[specialRules[0]];
}
else {
return specials[specialRules[0]]();
}
}
let stringResult = stringGen();
let minLength = 1;
if (rules.length) {
if (stringResult.length < rules.length.limit) {
while (stringResult.length < rules.length.limit) {
stringResult = stringResult + stringGen();
}
}
stringResult = stringResult.substr(0, rules.length.limit);
}
else if (rules.max && rules.min !== undefined) {
if (stringResult.length < rules.min.limit) {
while (stringResult.length < rules.min.limit) {
stringResult = stringResult + stringGen();
}
}
const length = rules.min.limit + Math.floor(Math.random() * (rules.max.limit - rules.min.limit));
stringResult = stringResult.substr(0, length);
}
else if (rules.max) {
if (stringResult.length > rules.max.limit) {
const length = Math.floor(rules.max.limit * Math.random()) + 1;
stringResult = stringResult.substr(0, length);
}
}
else if (rules.min) {
minLength = rules.min.limit;
if (stringResult.length < minLength) {
while (stringResult.length < rules.min.limit) {
stringResult = stringResult + stringGen();
}
}
const length = Math.ceil(minLength * (Math.random() + 1)) + 1;
stringResult = stringResult.substr(0, length);
}
if (rules.case) {
const { direction } = rules.case;
stringResult = direction === 'upper' ? stringResult.toLocaleUpperCase() : stringResult.toLocaleLowerCase();
}
return stringResult;
}
}
class NumberExample extends Any {
_generate(rules) {
let incrementor = 1;
let min = 1;
let max = 5;
let numberResult;
let lockMin;
let lockMax;
const randNum = (maxVal, minVal, increment) => {
let rand;
if (increment > 1) {
rand = Math.random() * Math.floor((maxVal - minVal) / increment);
}
else {
rand = Math.random() * (maxVal - minVal);
}
if (rules.integer !== undefined || rules.multiple !== undefined) {
return Math.floor(rand) * increment;
}
return rand;
};
const setMin = (value) => {
if (lockMin !== true || value > min) {
min = value;
}
};
const setMax = (value) => {
if (lockMax !== true || value < max) {
max = value;
}
};
if (rules.min !== undefined || rules.greater !== undefined) {
min = rules.min !== undefined ? rules.min.limit : rules.greater.limit + 1;
lockMin = true;
if (rules.max === undefined && rules.less === undefined) {
max = min + 5;
}
}
if (rules.max !== undefined || rules.less !== undefined) {
max = rules.max !== undefined ? rules.max.limit : rules.less.limit - 1;
lockMax = true;
}
if (rules.sign && rules.sign.sign === 'negative') {
let cacheMax = max;
setMax(max < 0 ? max : 0);
if (!(min < 0)) {
if (cacheMax === max) {
cacheMax = 5;
}
setMin(max - cacheMax);
}
}
if (rules.multiple !== undefined) {
incrementor = rules.multiple.base;
if (min % incrementor !== 0) {
let diff;
if (min > 0) {
diff = min < incrementor ?
incrementor - min :
incrementor - (min % incrementor);
}
else {
diff = Math.abs(min) < incrementor ?
0 - (min + incrementor) :
0 - (min % incrementor);
}
setMin(min + diff);
if (max > 0) {
if ((min + incrementor) >= max) {
setMax(min + Math.floor(max / incrementor));
}
}
}
}
numberResult = min + randNum(max, min, incrementor);
if (min === max) {
numberResult = min;
}
if (rules.precision !== undefined) {
let fixedDigits = numberResult.toFixed(rules.precision.limit);
if (fixedDigits.split('.')[1] === '00') {
fixedDigits = fixedDigits.split('.').map((digitSet, index) => {
return index === 0 ?
digitSet :
'05';
}).join('.');
}
numberResult = Number(fixedDigits);
}
const impossible = this._schema.validate(numberResult).error !== undefined;
return impossible ? NaN : numberResult;
}
}
class BooleanExample extends Any {
_generate() {
const schemaDescription = this._schema.describe();
const truthy = schemaDescription.truthy ? schemaDescription.truthy.slice(0) : [];
const falsy = schemaDescription.falsy ? schemaDescription.falsy.slice(0) : [];
const possibleResult = truthy.concat(falsy);
return possibleResult.length > 0
? Helpers.pickRandomFromArray(possibleResult)
: Math.random() > 0.5;
}
}
class BinaryExample extends Any {
_generate(rules) {
let bufferSize = 10;
if (rules.length) {
bufferSize = rules.length.limit;
}
else if (rules.min && rules.max) {
bufferSize = rules.min.limit + Math.floor(Math.random() * (rules.max.limit - rules.min.limit));
}
else if (rules.min) {
bufferSize = Math.ceil(rules.min.limit * (Math.random() + 1));
}
else if (rules.max) {
bufferSize = Math.ceil(rules.max.limit * Math.random());
}
const encodingFlag = Hoek.reach(this._schema.describe(), 'flags.encoding');
const encoding = encodingFlag || 'utf8';
const bufferResult = Buffer.alloc(bufferSize, Math.random().toString(36).substr(2));
return encodingFlag ? bufferResult.toString(encoding) : bufferResult;
}
}
class DateExample extends Any {
_generate(rules) {
const schemaDescription = this._schema.describe();
let dateModifier = Math.random() * (new Date()).getTime() / 5;
let min = 0;
let max = (new Date(min + dateModifier)).getTime();
if (rules.min) {
min = rules.min.date === 'now' ?
(new Date()).getTime() :
(new Date(rules.min.date)).getTime();
if (rules.max === undefined) {
max = min + dateModifier;
}
}
if (rules.max) {
max = rules.max.date === 'now' ?
(new Date()).getTime()
: (new Date(rules.max.date)).getTime();
if (rules.min === undefined) {
min = max - dateModifier;
}
}
dateModifier = Math.random() * (max - min);
let dateResult = new Date(min + dateModifier);
if (schemaDescription.flags && schemaDescription.flags.format) {
if (schemaDescription.flags.format === 'javascript') {
dateResult = dateResult.getTime() / Number(1);
}
else if (schemaDescription.flags.format === 'unix') {
dateResult = dateResult.getTime() / Number(1000);
}
else {
//ISO formatting is nested as a ISO Regex in format.
//But since date.format() API is no longer natively supported,
//regex pattern does not need to be acknowledged and ISO
//output is implied.
dateResult = dateResult.toISOString();
const isArray = Array.isArray(schemaDescription.flags.format);
if (!isArray) {
const moments = new Moment(dateResult);
dateResult = moments.format(schemaDescription.flags.format);
}
else {
const moment = new Moment(dateResult);
const targetFormat = Helpers.pickRandomFromArray(schemaDescription.flags.format);
dateResult = moment.format(targetFormat);
}
}
}
return dateResult;
}
}
class FunctionExample extends Any {
_generate(rules) {
const parameterNames = [];
let idealArityCount = 0;
const arityCount = rules.arity === undefined ? null : rules.arity.n;
const minArityCount = rules.minArity === undefined ? null : rules.minArity.n;
const maxArityCount = rules.maxArity === undefined ? null : rules.maxArity.n;
if (arityCount) {
idealArityCount = arityCount;
}
else if (minArityCount && maxArityCount) {
idealArityCount = Math.floor(Math.random() * (maxArityCount - minArityCount) + minArityCount);
}
else if (minArityCount) {
idealArityCount = minArityCount;
}
else if (maxArityCount) {
idealArityCount = maxArityCount;
}
for (let i = 0; i < idealArityCount; ++i) {
parameterNames.push('param' + i);
}
return new Function(parameterNames.join(','), 'return 0;');
}
}
class ArrayExample extends Any {
_generate(rules) {
const schemaDescription = this._schema.describe();
const childOptions = {
schemaDescription,
config: this._options
};
const arrayIsSparse = schemaDescription.flags && schemaDescription.flags.sparse;
const arrayIsSingle = schemaDescription.flags && schemaDescription.flags.single;
let arrayResult = [];
if (!arrayIsSparse) {
if (schemaDescription.ordered) {
for (let i = 0; i < schemaDescription.ordered.length; ++i) {
const itemRawSchema = Hoek.reach(this._schema, '$_terms.ordered')[i];
const itemType = internals.getType(itemRawSchema);
const Item = new Examples[itemType](itemRawSchema, childOptions);
arrayResult.push(Item.generate());
}
}
if (schemaDescription.items) {
for (let i = 0; i < schemaDescription.items.length; ++i) {
const itemIsForbidden = schemaDescription.items[i].flags && schemaDescription.items[i].flags.presence === 'forbidden';
if (!itemIsForbidden) {
const itemRawSchema = Hoek.reach(this._schema, '$_terms.items')[i];
const itemType = internals.getType(itemRawSchema);
const Item = new Examples[itemType](itemRawSchema, childOptions);
arrayResult.push(Item.generate());
}
}
}
const itemsToAdd = schemaDescription.items ? schemaDescription.items : [
{
type: 'string'
},
{
type: 'number'
}
];
if (rules.length && arrayResult.length !== rules.length.limit) {
if (arrayResult.length > rules.length.limit) {
arrayResult = arrayResult.slice(0, rules.length.limit);
}
else {
while (arrayResult.length < rules.length.limit) {
const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd);
const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd));
arrayResult.push(itemExample.generate());
}
}
}
if (rules.min && arrayResult.length < rules.min.limit) {
while (arrayResult.length < rules.min.limit) {
const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd);
const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd));
arrayResult.push(itemExample.generate());
}
}
if (rules.max && arrayResult.length === 0) {
const arrayLength = Math.ceil(Math.random() * rules.max.limit);
while (arrayResult.length < arrayLength) {
const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd);
const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd));
arrayResult.push(itemExample.generate());
}
}
}
if (arrayResult.length > 0 && arrayIsSingle) {
arrayResult = arrayResult.pop();
}
return arrayResult;
}
}
class ObjectExample extends Any {
_generate(rules) {
const schemaDescription = this._schema.describe();
const parentPresence = Hoek.reach(schemaDescription, 'preferences.presence');
const objectResult = {};
const randomChildGenerator = function () {
const randString = Math.random().toString(36).substr(2);
objectResult[randString.substr(0, 4)] = randString;
};
let objectChildGenerator = randomChildGenerator;
if (schemaDescription.keys) {
const schemaDescriptionKeys = Object.keys(schemaDescription.keys);
const isExistAlternatives = Boolean(Object.values(schemaDescription.keys).find((child) => child.type === 'alternatives'));
if (isExistAlternatives) {
schemaDescriptionKeys.sort((a, b) => {
const isAlternatives = schemaDescription.keys[a].type === 'alternatives';
return isAlternatives ? 1 : -1;
});
}
schemaDescriptionKeys.forEach((childKey) => {
const childSchemaRaw = this._schema._ids._byKey.get(childKey).schema;
const childSchema = schemaDescription.keys[childKey];
const flagsPresence = Hoek.reach(childSchema, 'flags.presence');
const childIsRequired = flagsPresence === 'required';
const childIsOptional = (flagsPresence === 'optional') || (parentPresence === 'optional' && !childIsRequired);
const childIsForbidden = flagsPresence === 'forbidden';
const shouldStrip = Hoek.reach(childSchema, 'flags.result') === 'strip';
if (shouldStrip || childIsForbidden || (childIsOptional && !(Hoek.reach(this._options, 'includeOptional')))) {
return;
}
const childOptions = {
schemaDescription,
objectResult,
config: this._options
};
const childType = internals.getType(childSchemaRaw);
const child = new Examples[childType](childSchemaRaw, childOptions);
objectResult[childKey] = child.generate();
});
}
if (schemaDescription.patterns) {
const pattern = Helpers.pickRandomFromArray(schemaDescription.patterns);
const patternRaw = this._schema.$_terms.patterns.filter((patternSchema) => {
return patternSchema.regex.toString() === pattern.regex;
})[0].rule;
const options = this._options;
objectChildGenerator = function () {
const initialKeyLength = Object.keys(objectResult).length;
const key = new RandExp(pattern.regex.substr(1, pattern.regex.length - 2)).gen();
const child = new Examples[pattern.rule.type](patternRaw, { config: options });
objectResult[key] = child.generate();
if (initialKeyLength === Object.keys(objectResult).length) {
objectChildGenerator = randomChildGenerator;
}
};
}
if (rules.instance) {
return new rules.instance.constructor();
}
if (rules.schema) {
return this._schema;
}
let keyCount = 0;
if (rules.min && Object.keys(objectResult).length < rules.min.limit) {
keyCount = rules.min.limit;
}
else if (rules.max && Object.keys(objectResult).length === 0) {
keyCount = rules.max.limit - 1;
}
else {
keyCount = rules.length && rules.length.limit;
}
while (Object.keys(objectResult).length < keyCount) {
objectChildGenerator();
}
if (schemaDescription.dependencies) {
const objectDependencies = {};
schemaDescription.dependencies.forEach((dependency) => {
if (dependency.rel === 'with') {
objectDependencies[dependency.rel] = {
peers: dependency.peers,
key: dependency.key
};
}
else {
objectDependencies[dependency.rel] = dependency.peers;
}
});
if (objectDependencies.nand || objectDependencies.xor || objectDependencies.without) {
const peers = objectDependencies.nand || objectDependencies.xor || objectDependencies.without;
if (peers.length > 1) {
peers.splice(Math.floor(Math.random() * peers.length), 1);
}
peers.forEach((keyToDelete) => {
delete objectResult[keyToDelete];
});
}
if (objectDependencies.with && Hoek.reach(objectResult, objectDependencies.with.key) !== undefined) {
const options = this._options;
objectDependencies.with.peers.forEach((peerKey) => {
if (Hoek.reach(objectResult, peerKey) === undefined) {
const peerSchema = Joi.build(schemaDescription).extract(peerKey);
const peerOptions = {
schemaDescription,
objectResult,
config: options
};
const peer = new Examples[peerSchema.describe().type](peerSchema, peerOptions);
objectResult[peerKey] = peer.generate();
}
});
}
}
return objectResult;
}
}
class AlternativesExample extends Any {
constructor(schema, options) {
super(schema, options);
this._hydratedParent = options && options.objectResult;
}
_generate(rules) {
const schemaDescription = this._schema.describe();
let resultSchema;
let resultSchemaRaw;
if (schemaDescription.matches.length > 1) {
const potentialValues = schemaDescription.matches;
resultSchema = Helpers.pickRandomFromArray(potentialValues);
}
else {
if (schemaDescription.matches[0].ref) {
const driverPath = schemaDescription.matches[0].ref.path.join('.');
const driverValue = Hoek.reach(this._hydratedParent, driverPath);
let driverIsTruthy = false;
const { error } = Joi.build(schemaDescription.matches[0].is).validate(driverValue);
if (!error) {
driverIsTruthy = true;
}
if (driverIsTruthy) {
resultSchema = schemaDescription.matches[0].then;
resultSchemaRaw = Hoek.reach(this._schema, '$_terms.matches')[0].then;
}
else {
resultSchema = schemaDescription.matches[0].otherwise;
resultSchemaRaw = Hoek.reach(this._schema, '$_terms.matches')[0].otherwise;
}
}
else {
resultSchema = schemaDescription.matches[0];
}
}
const schema = resultSchemaRaw === undefined ? Joi.build(resultSchema.schema) : resultSchemaRaw;
const type = internals.getType(schema);
const result = new Examples[type](schema, { config: this._options });
return result.generate();
}
}
const Examples = {
any: Any,
string: StringExample,
number: NumberExample,
boolean: BooleanExample,
binary: BinaryExample,
date: DateExample,
func: FunctionExample,
function: FunctionExample,
array: ArrayExample,
object: ObjectExample,
alternatives: AlternativesExample
};
const valueGenerator = (schema, options) => {
const exampleType = internals.getType(schema);
const Example = Examples[exampleType];
const example = new Example(schema, options);
return example.generate();
};
module.exports = valueGenerator;