mirror of
https://github.com/YMFE/yapi.git
synced 2024-12-15 05:10:47 +08:00
1113 lines
31 KiB
JavaScript
1113 lines
31 KiB
JavaScript
function _interopDefault(ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
|
|
|
var $RefParser = _interopDefault(require('json-schema-ref-parser'));
|
|
var deref = _interopDefault(require('deref'));
|
|
var tslib_1 = require('tslib');
|
|
|
|
// dynamic proxy for custom generators
|
|
function proxy(gen) {
|
|
return function (value, schema, property) {
|
|
var fn = value;
|
|
var args = [];
|
|
// support for nested object, first-key is the generator
|
|
if (typeof value === 'object') {
|
|
fn = Object.keys(value)[0];
|
|
// treat the given array as arguments,
|
|
if (Array.isArray(value[fn])) {
|
|
// if the generator is expecting arrays they should be nested, e.g. `[[1, 2, 3], true, ...]`
|
|
args = value[fn];
|
|
}
|
|
else {
|
|
args.push(value[fn]);
|
|
}
|
|
}
|
|
// support for keypaths, e.g. "internet.email"
|
|
var props = fn.split('.');
|
|
// retrieve a fresh dependency
|
|
var ctx = gen();
|
|
while (props.length > 1) {
|
|
ctx = ctx[props.shift()];
|
|
}
|
|
// retrieve last value from context object
|
|
value = typeof ctx === 'object' ? ctx[props[0]] : ctx;
|
|
// invoke dynamic generators
|
|
if (typeof value === 'function') {
|
|
value = value.apply(ctx, args);
|
|
}
|
|
// test for pending callbacks
|
|
if (Object.prototype.toString.call(value) === '[object Object]') {
|
|
for (var key in value) {
|
|
if (typeof value[key] === 'function') {
|
|
throw new Error('Cannot resolve value for "' + property + ': ' + fn + '", given: ' + value);
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
}
|
|
/**
|
|
* Container is used to wrap external generators (faker, chance, casual, etc.) and its dependencies.
|
|
*
|
|
* - `jsf.extend('faker')` will enhance or define the given dependency.
|
|
* - `jsf.define('faker')` will provide the "faker" keyword support.
|
|
*
|
|
* RandExp is not longer considered an "extension".
|
|
*/
|
|
var Container = (function () {
|
|
function Container() {
|
|
// dynamic requires - handle all dependencies
|
|
// they will NOT be included on the bundle
|
|
this.registry = {};
|
|
this.support = {};
|
|
}
|
|
/**
|
|
* Override dependency given by name
|
|
* @param name
|
|
* @param callback
|
|
*/
|
|
Container.prototype.extend = function (name, callback) {
|
|
var _this = this;
|
|
this.registry[name] = callback(this.registry[name]);
|
|
// built-in proxy (can be overridden)
|
|
if (!this.support[name]) {
|
|
this.support[name] = proxy(function () { return _this.registry[name]; });
|
|
}
|
|
};
|
|
/**
|
|
* Set keyword support by name
|
|
* @param name
|
|
* @param callback
|
|
*/
|
|
Container.prototype.define = function (name, callback) {
|
|
this.support[name] = callback;
|
|
};
|
|
/**
|
|
* Returns dependency given by name
|
|
* @param name
|
|
* @returns {Dependency}
|
|
*/
|
|
Container.prototype.get = function (name) {
|
|
if (typeof this.registry[name] === 'undefined') {
|
|
throw new ReferenceError('"' + name + '" dependency doesn\'t exist.');
|
|
}
|
|
return this.registry[name];
|
|
};
|
|
/**
|
|
* Apply a custom keyword
|
|
* @param schema
|
|
*/
|
|
Container.prototype.wrap = function (schema) {
|
|
var keys = Object.keys(schema);
|
|
var length = keys.length;
|
|
while (length--) {
|
|
var fn = keys[length].replace(/^x-/, '');
|
|
var gen = this.support[fn];
|
|
if (typeof gen === 'function') {
|
|
schema.generate = function () { return gen(schema[keys[length]], schema, keys[length]); };
|
|
break;
|
|
}
|
|
}
|
|
return schema;
|
|
};
|
|
return Container;
|
|
}());
|
|
|
|
/**
|
|
* This class defines a registry for custom formats used within JSF.
|
|
*/
|
|
var Registry = (function () {
|
|
function Registry() {
|
|
// empty by default
|
|
this.data = {};
|
|
}
|
|
/**
|
|
* Registers custom format
|
|
*/
|
|
Registry.prototype.register = function (name, callback) {
|
|
this.data[name] = callback;
|
|
};
|
|
/**
|
|
* Register many formats at one shot
|
|
*/
|
|
Registry.prototype.registerMany = function (formats) {
|
|
for (var name in formats) {
|
|
this.data[name] = formats[name];
|
|
}
|
|
};
|
|
/**
|
|
* Returns element by registry key
|
|
*/
|
|
Registry.prototype.get = function (name) {
|
|
var format = this.data[name];
|
|
return format;
|
|
};
|
|
/**
|
|
* Returns the whole registry content
|
|
*/
|
|
Registry.prototype.list = function () {
|
|
return this.data;
|
|
};
|
|
return Registry;
|
|
}());
|
|
|
|
// instantiate
|
|
var registry = new Registry();
|
|
/**
|
|
* Custom format API
|
|
*
|
|
* @see https://github.com/json-schema-faker/json-schema-faker#custom-formats
|
|
* @param nameOrFormatMap
|
|
* @param callback
|
|
* @returns {any}
|
|
*/
|
|
function formatAPI$1(nameOrFormatMap, callback) {
|
|
if (typeof nameOrFormatMap === 'undefined') {
|
|
return registry.list();
|
|
}
|
|
else if (typeof nameOrFormatMap === 'string') {
|
|
if (typeof callback === 'function') {
|
|
registry.register(nameOrFormatMap, callback);
|
|
}
|
|
else {
|
|
return registry.get(nameOrFormatMap);
|
|
}
|
|
}
|
|
else {
|
|
registry.registerMany(nameOrFormatMap);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class defines a registry for custom settings used within JSF.
|
|
*/
|
|
var OptionRegistry = (function (_super) {
|
|
tslib_1.__extends(OptionRegistry, _super);
|
|
function OptionRegistry() {
|
|
var _this = _super.call(this) || this;
|
|
_this.data['failOnInvalidTypes'] = true;
|
|
_this.data['defaultInvalidTypeProduct'] = null;
|
|
_this.data['failOnInvalidFormat'] = true;
|
|
_this.data['useDefaultValue'] = false;
|
|
_this.data['requiredOnly'] = false;
|
|
_this.data['maxItems'] = null;
|
|
_this.data['maxLength'] = null;
|
|
_this.data['defaultMinItems'] = 0;
|
|
_this.data['defaultRandExpMax'] = 10;
|
|
_this.data['alwaysFakeOptionals'] = false;
|
|
_this.data['random'] = Math.random;
|
|
return _this;
|
|
}
|
|
return OptionRegistry;
|
|
}(Registry));
|
|
|
|
// instantiate
|
|
var registry$1 = new OptionRegistry();
|
|
/**
|
|
* Custom option API
|
|
*
|
|
* @param nameOrOptionMap
|
|
* @returns {any}
|
|
*/
|
|
function optionAPI(nameOrOptionMap) {
|
|
if (typeof nameOrOptionMap === 'string') {
|
|
return registry$1.get(nameOrOptionMap);
|
|
}
|
|
else {
|
|
return registry$1.registerMany(nameOrOptionMap);
|
|
}
|
|
}
|
|
|
|
var RandExp = require('randexp');
|
|
// set maximum default, see #193
|
|
RandExp.prototype.max = 10;
|
|
// same implementation as the original except using our random
|
|
RandExp.prototype.randInt = function (a, b) {
|
|
return a + Math.floor(optionAPI('random')() * (1 + b - a));
|
|
};
|
|
function _randexp(value) {
|
|
var re = new RandExp(value);
|
|
// apply given setting
|
|
re.max = optionAPI('defaultRandExpMax');
|
|
return re.gen();
|
|
}
|
|
function getSubAttribute(obj, dotSeparatedKey) {
|
|
var keyElements = dotSeparatedKey.split('.');
|
|
while (keyElements.length) {
|
|
var prop = keyElements.shift();
|
|
if (!obj[prop]) {
|
|
break;
|
|
}
|
|
obj = obj[prop];
|
|
}
|
|
return obj;
|
|
}
|
|
/**
|
|
* Returns true/false whether the object parameter has its own properties defined
|
|
*
|
|
* @param obj
|
|
* @param properties
|
|
* @returns {boolean}
|
|
*/
|
|
function hasProperties(obj) {
|
|
var properties = [];
|
|
for (var _i = 1; _i < arguments.length; _i++) {
|
|
properties[_i - 1] = arguments[_i];
|
|
}
|
|
return properties.filter(function (key) {
|
|
return typeof obj[key] !== 'undefined';
|
|
}).length > 0;
|
|
}
|
|
/**
|
|
* Returns typecasted value.
|
|
* External generators (faker, chance, casual) may return data in non-expected formats, such as string, when you might expect an
|
|
* integer. This function is used to force the typecast.
|
|
*
|
|
* @param value
|
|
* @param targetType
|
|
* @returns {any}
|
|
*/
|
|
function typecast(value, schema) {
|
|
// FIXME this function should cover most cases and should be reused within generators
|
|
switch (schema.type) {
|
|
case 'integer':
|
|
return parseInt(value, 10);
|
|
case 'number':
|
|
return parseFloat(value);
|
|
case 'string':
|
|
value = String(value);
|
|
var min = Math.max(schema.minLength || 0, 0);
|
|
var max = Math.min(schema.maxLength || Infinity, Infinity);
|
|
while (value.length < min) {
|
|
value += ' ' + value;
|
|
}
|
|
if (value.length > max) {
|
|
value = value.substr(0, max);
|
|
}
|
|
return value;
|
|
case 'boolean':
|
|
return !!value;
|
|
default:
|
|
return value;
|
|
}
|
|
}
|
|
function merge(a, b) {
|
|
for (var key in b) {
|
|
if (typeof b[key] !== 'object' || b[key] === null) {
|
|
a[key] = b[key];
|
|
}
|
|
else if (Array.isArray(b[key])) {
|
|
a[key] = a[key] || [];
|
|
// fix #292 - skip duplicated values from merge object (b)
|
|
b[key].forEach(function (value) {
|
|
if (a[key].indexOf(value)) {
|
|
a[key].push(value);
|
|
}
|
|
});
|
|
}
|
|
else if (typeof a[key] !== 'object' || a[key] === null || Array.isArray(a[key])) {
|
|
a[key] = merge({}, b[key]);
|
|
}
|
|
else {
|
|
a[key] = merge(a[key], b[key]);
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
function clean(obj, isArray, requiredProps) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
if (Array.isArray(obj)) {
|
|
obj = obj
|
|
.map(function (value) { return clean(value, true); })
|
|
.filter(function (value) { return typeof value !== 'undefined'; });
|
|
return obj;
|
|
}
|
|
Object.keys(obj).forEach(function (k) {
|
|
if (!requiredProps || requiredProps.indexOf(k) === -1) {
|
|
if (Array.isArray(obj[k]) && !obj[k].length) {
|
|
delete obj[k];
|
|
}
|
|
}
|
|
else {
|
|
obj[k] = clean(obj[k]);
|
|
}
|
|
});
|
|
if (!Object.keys(obj).length && isArray) {
|
|
return undefined;
|
|
}
|
|
return obj;
|
|
}
|
|
function short(schema) {
|
|
var s = JSON.stringify(schema);
|
|
var l = JSON.stringify(schema, null, 2);
|
|
return s.length > 400 ? l.substr(0, 400) + '...' : l;
|
|
}
|
|
var utils = {
|
|
getSubAttribute: getSubAttribute,
|
|
hasProperties: hasProperties,
|
|
typecast: typecast,
|
|
merge: merge,
|
|
clean: clean,
|
|
short: short,
|
|
randexp: _randexp
|
|
};
|
|
|
|
/// <reference path="../index.d.ts" />
|
|
/**
|
|
* Returns random element of a collection
|
|
*
|
|
* @param collection
|
|
* @returns {T}
|
|
*/
|
|
function pick(collection) {
|
|
return collection[Math.floor(optionAPI('random')() * collection.length)];
|
|
}
|
|
/**
|
|
* Returns shuffled collection of elements
|
|
*
|
|
* @param collection
|
|
* @returns {T[]}
|
|
*/
|
|
function shuffle(collection) {
|
|
var tmp, key, copy = collection.slice(), length = collection.length;
|
|
for (; length > 0;) {
|
|
key = Math.floor(optionAPI('random')() * length);
|
|
// swap
|
|
tmp = copy[--length];
|
|
copy[length] = copy[key];
|
|
copy[key] = tmp;
|
|
}
|
|
return copy;
|
|
}
|
|
/**
|
|
* These values determine default range for random.number function
|
|
*
|
|
* @type {number}
|
|
*/
|
|
var MIN_NUMBER = -100;
|
|
var MAX_NUMBER = 100;
|
|
/**
|
|
* Returns a random integer between min (inclusive) and max (inclusive)
|
|
* Using Math.round() will give you a non-uniform distribution!
|
|
* @see http://stackoverflow.com/a/1527820/769384
|
|
*/
|
|
function getRandom(min, max) {
|
|
return optionAPI('random')() * (max - min) + min;
|
|
}
|
|
/**
|
|
* Generates random number according to parameters passed
|
|
*
|
|
* @param min
|
|
* @param max
|
|
* @param defMin
|
|
* @param defMax
|
|
* @param hasPrecision
|
|
* @returns {number}
|
|
*/
|
|
function number(min, max, defMin, defMax, hasPrecision) {
|
|
if (hasPrecision === void 0) { hasPrecision = false; }
|
|
defMin = typeof defMin === 'undefined' ? MIN_NUMBER : defMin;
|
|
defMax = typeof defMax === 'undefined' ? MAX_NUMBER : defMax;
|
|
min = typeof min === 'undefined' ? defMin : min;
|
|
max = typeof max === 'undefined' ? defMax : max;
|
|
if (max < min) {
|
|
max += min;
|
|
}
|
|
var result = getRandom(min, max);
|
|
if (!hasPrecision) {
|
|
return Math.round(result);
|
|
}
|
|
return result;
|
|
}
|
|
var random = {
|
|
pick: pick,
|
|
shuffle: shuffle,
|
|
number: number,
|
|
};
|
|
|
|
var ParseError = (function (_super) {
|
|
tslib_1.__extends(ParseError, _super);
|
|
function ParseError(message, path) {
|
|
var _this = _super.call(this) || this;
|
|
_this.path = path;
|
|
if (Error.captureStackTrace) {
|
|
Error.captureStackTrace(_this, _this.constructor);
|
|
}
|
|
_this.name = 'ParseError';
|
|
_this.message = message;
|
|
_this.path = path;
|
|
return _this;
|
|
}
|
|
return ParseError;
|
|
}(Error));
|
|
|
|
var inferredProperties = {
|
|
array: [
|
|
'additionalItems',
|
|
'items',
|
|
'maxItems',
|
|
'minItems',
|
|
'uniqueItems'
|
|
],
|
|
integer: [
|
|
'exclusiveMaximum',
|
|
'exclusiveMinimum',
|
|
'maximum',
|
|
'minimum',
|
|
'multipleOf'
|
|
],
|
|
object: [
|
|
'additionalProperties',
|
|
'dependencies',
|
|
'maxProperties',
|
|
'minProperties',
|
|
'patternProperties',
|
|
'properties',
|
|
'required'
|
|
],
|
|
string: [
|
|
'maxLength',
|
|
'minLength',
|
|
'pattern'
|
|
]
|
|
};
|
|
inferredProperties.number = inferredProperties.integer;
|
|
var subschemaProperties = [
|
|
'additionalItems',
|
|
'items',
|
|
'additionalProperties',
|
|
'dependencies',
|
|
'patternProperties',
|
|
'properties'
|
|
];
|
|
/**
|
|
* Iterates through all keys of `obj` and:
|
|
* - checks whether those keys match properties of a given inferred type
|
|
* - makes sure that `obj` is not a subschema; _Do not attempt to infer properties named as subschema containers. The
|
|
* reason for this is that any property name within those containers that matches one of the properties used for
|
|
* inferring missing type values causes the container itself to get processed which leads to invalid output. (Issue 62)_
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function matchesType(obj, lastElementInPath, inferredTypeProperties) {
|
|
return Object.keys(obj).filter(function (prop) {
|
|
var isSubschema = subschemaProperties.indexOf(lastElementInPath) > -1, inferredPropertyFound = inferredTypeProperties.indexOf(prop) > -1;
|
|
if (inferredPropertyFound && !isSubschema) {
|
|
return true;
|
|
}
|
|
}).length > 0;
|
|
}
|
|
/**
|
|
* Checks whether given `obj` type might be inferred. The mechanism iterates through all inferred types definitions,
|
|
* tries to match allowed properties with properties of given `obj`. Returns type name, if inferred, or null.
|
|
*
|
|
* @returns {string|null}
|
|
*/
|
|
function inferType(obj, schemaPath) {
|
|
for (var typeName in inferredProperties) {
|
|
var lastElementInPath = schemaPath[schemaPath.length - 1];
|
|
if (matchesType(obj, lastElementInPath, inferredProperties[typeName])) {
|
|
return typeName;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates randomized boolean value.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
function booleanGenerator() {
|
|
return optionAPI('random')() > 0.5;
|
|
}
|
|
|
|
var booleanType = booleanGenerator;
|
|
|
|
/**
|
|
* Generates null value.
|
|
*
|
|
* @returns {null}
|
|
*/
|
|
function nullGenerator() {
|
|
return null;
|
|
}
|
|
|
|
var nullType = nullGenerator;
|
|
|
|
// TODO provide types
|
|
function unique(path, items, value, sample, resolve, traverseCallback) {
|
|
var tmp = [], seen = [];
|
|
function walk(obj) {
|
|
var json = JSON.stringify(obj);
|
|
if (seen.indexOf(json) === -1) {
|
|
seen.push(json);
|
|
tmp.push(obj);
|
|
}
|
|
}
|
|
items.forEach(walk);
|
|
// TODO: find a better solution?
|
|
var limit = 100;
|
|
while (tmp.length !== items.length) {
|
|
walk(traverseCallback(value.items || sample, path, resolve));
|
|
if (!limit--) {
|
|
break;
|
|
}
|
|
}
|
|
return tmp;
|
|
}
|
|
// TODO provide types
|
|
var arrayType = function arrayType(value, path, resolve, traverseCallback) {
|
|
var items = [];
|
|
if (!(value.items || value.additionalItems)) {
|
|
if (utils.hasProperties(value, 'minItems', 'maxItems', 'uniqueItems')) {
|
|
throw new ParseError('missing items for ' + utils.short(value), path);
|
|
}
|
|
return items;
|
|
}
|
|
// see http://stackoverflow.com/a/38355228/769384
|
|
// after type guards support subproperties (in TS 2.0) we can simplify below to (value.items instanceof Array)
|
|
// so that value.items.map becomes recognized for typescript compiler
|
|
var tmpItems = value.items;
|
|
if (tmpItems instanceof Array) {
|
|
return Array.prototype.concat.call(items, tmpItems.map(function (item, key) {
|
|
var itemSubpath = path.concat(['items', key + '']);
|
|
return traverseCallback(item, itemSubpath, resolve);
|
|
}));
|
|
}
|
|
var minItems = value.minItems;
|
|
//var maxItems = value.maxItems;
|
|
var maxItems = 1;
|
|
if (optionAPI('defaultMinItems') && minItems === undefined) {
|
|
// fix boundaries
|
|
minItems = !maxItems
|
|
? optionAPI('defaultMinItems')
|
|
: Math.min(optionAPI('defaultMinItems'), maxItems);
|
|
}
|
|
if (optionAPI('maxItems')) {
|
|
// Don't allow user to set max items above our maximum
|
|
if (maxItems && maxItems > optionAPI('maxItems')) {
|
|
maxItems = optionAPI('maxItems');
|
|
}
|
|
// Don't allow user to set min items above our maximum
|
|
if (minItems && minItems > optionAPI('maxItems')) {
|
|
minItems = maxItems;
|
|
}
|
|
}
|
|
var length = (maxItems != null && optionAPI('alwaysFakeOptionals')) ?
|
|
maxItems : random.number(minItems, maxItems, 1, 5),
|
|
// TODO below looks bad. Should additionalItems be copied as-is?
|
|
sample = typeof value.additionalItems === 'object' ? value.additionalItems : {};
|
|
for (var current = items.length; current < length; current++) {
|
|
var itemSubpath = path.concat(['items', current + '']);
|
|
var element = traverseCallback(value.items || sample, itemSubpath, resolve);
|
|
items.push(element);
|
|
}
|
|
if (value.uniqueItems) {
|
|
return unique(path.concat(['items']), items, value, sample, resolve, traverseCallback);
|
|
}
|
|
return items;
|
|
};
|
|
|
|
var MIN_INTEGER = -100000000;
|
|
var MAX_INTEGER = 100000000;
|
|
var numberType$1 = function numberType(value) {
|
|
var min = typeof value.minimum === 'undefined' ? MIN_INTEGER : value.minimum, max = typeof value.maximum === 'undefined' ? MAX_INTEGER : value.maximum, multipleOf = value.multipleOf;
|
|
if (multipleOf) {
|
|
max = Math.floor(max / multipleOf) * multipleOf;
|
|
min = Math.ceil(min / multipleOf) * multipleOf;
|
|
}
|
|
if (value.exclusiveMinimum && value.minimum && min === value.minimum) {
|
|
min += multipleOf || 1;
|
|
}
|
|
if (value.exclusiveMaximum && value.maximum && max === value.maximum) {
|
|
max -= multipleOf || 1;
|
|
}
|
|
if (min > max) {
|
|
return NaN;
|
|
}
|
|
if (multipleOf) {
|
|
return Math.floor(random.number(min, max) / multipleOf) * multipleOf;
|
|
}
|
|
return random.number(min, max, undefined, undefined, true);
|
|
};
|
|
|
|
// The `integer` type is just a wrapper for the `number` type. The `number` type
|
|
// returns floating point numbers, and `integer` type truncates the fraction
|
|
// part, leaving the result as an integer.
|
|
var integerType = function integerType(value) {
|
|
var generated = numberType$1(value);
|
|
// whether the generated number is positive or negative, need to use either
|
|
// floor (positive) or ceil (negative) function to get rid of the fraction
|
|
return generated > 0 ? Math.floor(generated) : Math.ceil(generated);
|
|
};
|
|
|
|
var LIPSUM_WORDS = ('Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore'
|
|
+ ' et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea'
|
|
+ ' commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla'
|
|
+ ' pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est'
|
|
+ ' laborum').split(' ');
|
|
/**
|
|
* Generates randomized array of single lorem ipsum words.
|
|
*
|
|
* @param length
|
|
* @returns {Array.<string>}
|
|
*/
|
|
function wordsGenerator$1(length) {
|
|
var words = random.shuffle(LIPSUM_WORDS);
|
|
return words.slice(0, length);
|
|
}
|
|
|
|
// fallback generator
|
|
var anyType = { type: ['string', 'number', 'integer', 'boolean'] };
|
|
// TODO provide types
|
|
var objectType = function objectType(value, path, resolve, traverseCallback) {
|
|
var props = {};
|
|
var properties = value.properties || {};
|
|
var propertyKeys = value.required = Object.keys(properties);
|
|
var patternProperties = value.patternProperties || {};
|
|
var requiredProperties = (value.required || []).slice();
|
|
var allowsAdditional = value.additionalProperties === false ? false : true;
|
|
|
|
var patternPropertyKeys = Object.keys(patternProperties);
|
|
var additionalProperties = allowsAdditional
|
|
? (value.additionalProperties === true ? {} : value.additionalProperties)
|
|
: null;
|
|
if (!allowsAdditional &&
|
|
propertyKeys.length === 0 &&
|
|
patternPropertyKeys.length === 0 &&
|
|
utils.hasProperties(value, 'minProperties', 'maxProperties', 'dependencies', 'required')) {
|
|
throw new ParseError('missing properties for:\n' + utils.short(value), path);
|
|
}
|
|
if (optionAPI('requiredOnly') === true) {
|
|
requiredProperties.forEach(function (key) {
|
|
if (properties[key]) {
|
|
props[key] = properties[key];
|
|
}
|
|
});
|
|
return traverseCallback(props, path.concat(['properties']), resolve);
|
|
}
|
|
var min = Math.max(value.minProperties || 0, requiredProperties.length);
|
|
var max = Math.max(value.maxProperties || random.number(min, min + 5));
|
|
random.shuffle(patternPropertyKeys.concat(propertyKeys)).forEach(function (_key) {
|
|
if (requiredProperties.indexOf(_key) === -1) {
|
|
requiredProperties.push(_key);
|
|
}
|
|
});
|
|
// properties are read from right-to-left
|
|
var _props = optionAPI('alwaysFakeOptionals') ? requiredProperties
|
|
: requiredProperties.slice(0, random.number(min, max));
|
|
_props.forEach(function (key) {
|
|
// first ones are the required properies
|
|
if (properties[key]) {
|
|
props[key] = properties[key];
|
|
}
|
|
else {
|
|
var found;
|
|
// then try patternProperties
|
|
patternPropertyKeys.forEach(function (_key) {
|
|
if (key.match(new RegExp(_key))) {
|
|
found = true;
|
|
props[utils.randexp(key)] = patternProperties[_key];
|
|
}
|
|
});
|
|
if (!found) {
|
|
// try patternProperties again,
|
|
var subschema = patternProperties[key] || additionalProperties;
|
|
if (subschema) {
|
|
// otherwise we can use additionalProperties?
|
|
props[patternProperties[key] ? utils.randexp(key) : key] = subschema;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var current = Object.keys(props).length;
|
|
while (true) {
|
|
if (!(patternPropertyKeys.length || allowsAdditional)) {
|
|
break;
|
|
}
|
|
if (current >= min) {
|
|
break;
|
|
}
|
|
if (allowsAdditional) {
|
|
var word = wordsGenerator$1(1) + utils.randexp('[a-f\\d]{1,3}');
|
|
if (!props[word]) {
|
|
props[word] = additionalProperties || anyType;
|
|
current += 1;
|
|
}
|
|
}
|
|
patternPropertyKeys.forEach(function (_key) {
|
|
var word = utils.randexp(_key);
|
|
if (!props[word]) {
|
|
props[word] = patternProperties[_key];
|
|
current += 1;
|
|
}
|
|
});
|
|
}
|
|
if (!allowsAdditional && current < min) {
|
|
throw new ParseError('properties constraints were too strong to successfully generate a valid object for:\n' +
|
|
utils.short(value), path);
|
|
}
|
|
return traverseCallback(props, path.concat(['properties']), resolve);
|
|
};
|
|
|
|
/**
|
|
* Helper function used by thunkGenerator to produce some words for the final result.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function produce() {
|
|
var length = random.number(1, 5);
|
|
return wordsGenerator$1(length).join(' ');
|
|
}
|
|
/**
|
|
* Generates randomized concatenated string based on words generator.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function thunkGenerator$1(min, max) {
|
|
if (min === void 0) { min = 0; }
|
|
if (max === void 0) { max = 140; }
|
|
var min = Math.max(0, min), max = random.number(min, max), result = produce();
|
|
// append until length is reached
|
|
while (result.length < min) {
|
|
result += produce();
|
|
}
|
|
// cut if needed
|
|
if (result.length > max) {
|
|
result = result.substr(0, max);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generates randomized ipv4 address.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function ipv4Generator() {
|
|
return [0, 0, 0, 0].map(function () {
|
|
return random.number(0, 255);
|
|
}).join('.');
|
|
}
|
|
|
|
var MOST_NEAR_DATETIME = 2524608000000;
|
|
/**
|
|
* Generates randomized date time ISO format string.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function dateTimeGenerator$1() {
|
|
var date = new Date();
|
|
var days = random.number(-1000, MOST_NEAR_DATETIME);
|
|
date.setTime(date.getTime() - days);
|
|
return date.toISOString();
|
|
}
|
|
|
|
/**
|
|
* Predefined core formats
|
|
* @type {[key: string]: string}
|
|
*/
|
|
var regexps = {
|
|
email: '[a-zA-Z\\d][a-zA-Z\\d-]{1,13}[a-zA-Z\\d]@{hostname}',
|
|
hostname: '[a-zA-Z]{1,33}\\.[a-z]{2,4}',
|
|
ipv6: '[a-f\\d]{4}(:[a-f\\d]{4}){7}',
|
|
uri: '[a-zA-Z][a-zA-Z0-9+-.]*'
|
|
};
|
|
/**
|
|
* Generates randomized string basing on a built-in regex format
|
|
*
|
|
* @param coreFormat
|
|
* @returns {string}
|
|
*/
|
|
function coreFormatGenerator(coreFormat) {
|
|
return utils.randexp(regexps[coreFormat]).replace(/\{(\w+)\}/, function (match, key) {
|
|
return utils.randexp(regexps[key]);
|
|
});
|
|
}
|
|
|
|
function generateFormat(value, invalid) {
|
|
var callback = formatAPI$1(value.format);
|
|
if (typeof callback === 'function') {
|
|
return callback(value);
|
|
}
|
|
switch (value.format) {
|
|
case 'date-time':
|
|
return dateTimeGenerator$1();
|
|
case 'ipv4':
|
|
return ipv4Generator();
|
|
case 'regex':
|
|
// TODO: discuss
|
|
return '.+?';
|
|
case 'email':
|
|
case 'hostname':
|
|
case 'ipv6':
|
|
case 'uri':
|
|
return coreFormatGenerator(value.format);
|
|
default:
|
|
if (typeof callback === 'undefined') {
|
|
if (optionAPI('failOnInvalidFormat')) {
|
|
throw new Error('unknown registry key ' + utils.short(value.format));
|
|
}
|
|
else {
|
|
return invalid();
|
|
}
|
|
}
|
|
throw new Error('unsupported format "' + value.format + '"');
|
|
}
|
|
}
|
|
var stringType = function stringType(value) {
|
|
var output;
|
|
var minLength = value.minLength;
|
|
var maxLength = value.maxLength;
|
|
if (optionAPI('maxLength')) {
|
|
// Don't allow user to set max length above our maximum
|
|
if (maxLength && maxLength > optionAPI('maxLength')) {
|
|
maxLength = optionAPI('maxLength');
|
|
}
|
|
// Don't allow user to set min length above our maximum
|
|
if (minLength && minLength > optionAPI('maxLength')) {
|
|
minLength = optionAPI('maxLength');
|
|
}
|
|
}
|
|
if (value.format) {
|
|
output = generateFormat(value, function () { return thunkGenerator$1(minLength, maxLength); });
|
|
}
|
|
else if (value.pattern) {
|
|
output = utils.randexp(value.pattern);
|
|
}
|
|
else {
|
|
output = thunkGenerator$1(minLength, maxLength);
|
|
}
|
|
while (output.length < minLength) {
|
|
output += optionAPI('random')() > 0.7 ? thunkGenerator$1() : utils.randexp('.+');
|
|
}
|
|
if (output.length > maxLength) {
|
|
output = output.substr(0, maxLength);
|
|
}
|
|
return output;
|
|
};
|
|
|
|
// var typeMap = {
|
|
// boolean: booleanType,
|
|
// null: nullType,
|
|
// array: arrayType,
|
|
// integer: integerType,
|
|
// number: numberType$1,
|
|
// object: objectType,
|
|
// string: stringType
|
|
// };
|
|
|
|
var typeMap = {
|
|
boolean: () => '@boolean',
|
|
null: nullType,
|
|
array: arrayType,
|
|
integer: () => '@integer',
|
|
number: () => '@natural',
|
|
object: objectType,
|
|
string: () => '@string',
|
|
};
|
|
|
|
// TODO provide types
|
|
function traverse(schema, path, resolve) {
|
|
schema = resolve(schema);
|
|
if (!schema) {
|
|
return;
|
|
}
|
|
if (Array.isArray(schema.enum)) {
|
|
return random.pick(schema.enum);
|
|
}
|
|
// thunks can return sub-schemas
|
|
if (typeof schema.thunk === 'function') {
|
|
return traverse(schema.thunk(), path, resolve);
|
|
}
|
|
if (typeof schema.generate === 'function') {
|
|
return utils.typecast(schema.generate(), schema);
|
|
}
|
|
if (optionAPI('useDefaultValue') && 'default' in schema) {
|
|
return schema.default;
|
|
}
|
|
// TODO remove the ugly overcome
|
|
var type = schema.type;
|
|
if (Array.isArray(type)) {
|
|
type = random.pick(type);
|
|
}
|
|
else if (typeof type === 'undefined') {
|
|
// Attempt to infer the type
|
|
type = inferType(schema, path) || type;
|
|
}
|
|
if (typeof type === 'string') {
|
|
if (!typeMap[type]) {
|
|
if (optionAPI('failOnInvalidTypes')) {
|
|
throw new ParseError('unknown primitive ' + utils.short(type), path.concat(['type']));
|
|
}
|
|
else {
|
|
return optionAPI('defaultInvalidTypeProduct');
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
return utils.clean(typeMap[type](schema, path, resolve, traverse), null, schema.required);
|
|
}
|
|
catch (e) {
|
|
if (typeof e.path === 'undefined') {
|
|
throw new ParseError(e.message, path);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
var copy = {};
|
|
if (Array.isArray(schema)) {
|
|
copy = [];
|
|
}
|
|
for (var prop in schema) {
|
|
if (typeof schema[prop] === 'object' && prop !== 'definitions') {
|
|
copy[prop] = traverse(schema[prop], path.concat([prop]), resolve);
|
|
}
|
|
else {
|
|
copy[prop] = schema[prop];
|
|
}
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
function isKey(prop) {
|
|
return prop === 'enum' || prop === 'default' || prop === 'required' || prop === 'definitions';
|
|
}
|
|
// TODO provide types
|
|
function run(refs, schema, container) {
|
|
try {
|
|
return traverse(schema, [], function reduce(sub, maxReduceDepth) {
|
|
if (typeof maxReduceDepth === 'undefined') {
|
|
maxReduceDepth = random.number(1, 3);
|
|
}
|
|
if (!sub) {
|
|
return null;
|
|
}
|
|
// cleanup
|
|
if (sub.id && typeof sub.id === 'string') {
|
|
delete sub.id;
|
|
delete sub.$schema;
|
|
}
|
|
if (typeof sub.$ref === 'string') {
|
|
if (sub.$ref.indexOf('#/') === -1) {
|
|
var ref = deref.util.findByRef(sub.$ref, refs);
|
|
if (!ref) {
|
|
throw new Error('Reference not found: ' + sub.$ref);
|
|
}
|
|
return ref;
|
|
}
|
|
// just remove the reference
|
|
delete sub.$ref;
|
|
return sub;
|
|
}
|
|
if (Array.isArray(sub.allOf)) {
|
|
var schemas = sub.allOf;
|
|
delete sub.allOf;
|
|
// this is the only case where all sub-schemas
|
|
// must be resolved before any merge
|
|
schemas.forEach(function (subSchema) {
|
|
var _sub = reduce(subSchema, maxReduceDepth + 1);
|
|
// call given thunks if present
|
|
utils.merge(sub, typeof _sub.thunk === 'function'
|
|
? _sub.thunk()
|
|
: _sub);
|
|
});
|
|
}
|
|
if (Array.isArray(sub.oneOf || sub.anyOf)) {
|
|
var mix = sub.oneOf || sub.anyOf;
|
|
delete sub.anyOf;
|
|
delete sub.oneOf;
|
|
return {
|
|
thunk: function () {
|
|
var copy = utils.merge({}, sub);
|
|
utils.merge(copy, random.pick(mix));
|
|
return copy;
|
|
},
|
|
};
|
|
}
|
|
for (var prop in sub) {
|
|
if ((Array.isArray(sub[prop]) || typeof sub[prop] === 'object') && !isKey(prop)) {
|
|
sub[prop] = reduce(sub[prop], maxReduceDepth);
|
|
}
|
|
}
|
|
return container.wrap(sub);
|
|
});
|
|
}
|
|
catch (e) {
|
|
if (e.path) {
|
|
throw new Error(e.message + ' in ' + '/' + e.path.join('/'));
|
|
}
|
|
else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
var container = new Container();
|
|
function getRefs(refs) {
|
|
var $refs = {};
|
|
if (Array.isArray(refs)) {
|
|
refs.map(deref.util.normalizeSchema).forEach(function (schema) {
|
|
$refs[schema.id] = schema;
|
|
});
|
|
}
|
|
else {
|
|
$refs = refs || {};
|
|
}
|
|
return $refs;
|
|
}
|
|
var jsf = function (schema, refs) {
|
|
var $ = deref();
|
|
var $refs = getRefs(refs);
|
|
return run($refs, $(schema, $refs, true), container);
|
|
};
|
|
jsf.resolve = function (schema, refs, cwd) {
|
|
if (typeof refs === 'string') {
|
|
cwd = refs;
|
|
refs = {};
|
|
}
|
|
// normalize basedir (browser aware)
|
|
cwd = cwd || (typeof process !== 'undefined' ? process.cwd() : '');
|
|
cwd = cwd.replace(/\/+$/, '') + '/';
|
|
var $refs = getRefs(refs);
|
|
// identical setup as json-schema-sequelizer
|
|
var fixedRefs = {
|
|
order: 300,
|
|
canRead: true,
|
|
read: function (file, callback) {
|
|
callback(null, deref.util.findByRef(cwd !== '/'
|
|
? file.url.replace(cwd, '')
|
|
: file.url, $refs));
|
|
},
|
|
};
|
|
return $RefParser
|
|
.dereference(cwd, schema, {
|
|
resolve: { fixedRefs: fixedRefs },
|
|
dereference: {
|
|
circular: 'ignore',
|
|
},
|
|
}).then(function (sub) { return jsf(sub, refs); });
|
|
};
|
|
jsf.utils = utils;
|
|
jsf.format = formatAPI$1;
|
|
jsf.option = optionAPI;
|
|
// built-in support
|
|
container.define('pattern', utils.randexp);
|
|
// returns itself for chaining
|
|
jsf.extend = function (name, cb) {
|
|
container.extend(name, cb);
|
|
return jsf;
|
|
};
|
|
jsf.define = function (name, cb) {
|
|
container.define(name, cb);
|
|
return jsf;
|
|
};
|
|
jsf.locate = function (name) {
|
|
return container.get(name);
|
|
};
|
|
var VERSION = "0.5.0-rc11";
|
|
jsf.version = VERSION;
|
|
|
|
module.exports = jsf; |