cleaners
Version:
Cleans & validates untrusted data, with TypeScript & Flow support
311 lines (253 loc) • 7.69 kB
JavaScript
/**
* Given a JS value, produce a descriptive string.
*/
function showValue(value) {
switch (typeof value) {
case 'function':
case 'object':
if (value == null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
case 'string':
return JSON.stringify(value);
default:
return String(value);
}
}
function asOptional(cleaner, fallback) {
return function asOptional(raw) {
return raw != null ? cleaner(raw) : typeof fallback === 'function' ? fallback() : fallback;
};
}
function asValue() {
for (var _len = arguments.length, values = new Array(_len), _key = 0; _key < _len; _key++) {
values[_key] = arguments[_key];
}
return function asValue(raw) {
for (var i = 0; i < values.length; ++i) {
if (raw === values[i]) return raw;
} // Create a fancy error message:
var message = 'Expected ';
if (values.length !== 1) message += 'one of: ';
for (var _i = 0; _i < values.length; ++_i) {
if (_i > 0) message += ' | ';
message += JSON.stringify(values[_i]);
}
message += ', got ' + showValue(raw);
throw new TypeError(message);
};
}
function asBoolean(raw) {
if (typeof raw !== 'boolean') {
throw new TypeError('Expected a boolean, got ' + showValue(raw));
}
return raw;
}
function asNumber(raw) {
if (typeof raw !== 'number') {
throw new TypeError('Expected a number, got ' + showValue(raw));
}
return raw;
}
function asString(raw) {
if (typeof raw !== 'string') {
throw new TypeError('Expected a string, got ' + showValue(raw));
}
return raw;
}
var asNull = asValue(null);
var asUndefined = asValue(undefined);
function asUnknown(raw) {
return raw;
} // Deprecated:
var asNone = asOptional(asNull);
/**
* Adds location information to an error message.
*
* Errors can occur inside deeply-nested cleaners,
* such as "TypeError: Expected a string at .array[0].some.property".
* To build this information, each cleaner along the path
* should add its own location information as the stack unwinds.
*
* If the error has a `insertStepAt` property, that is the character offset
* where the next step will go in the error message. Otherwise,
* the next step just goes on the end of the string with the word "at".
*/
function locateError(error, step, offset) {
if (error instanceof Error) {
if (error.insertStepAt == null) {
error.message += ' at ';
error.insertStepAt = error.message.length;
}
error.message = error.message.slice(0, error.insertStepAt) + step + error.message.slice(error.insertStepAt);
error.insertStepAt += offset;
}
return error;
}
function asArray(cleaner) {
return function asArray(raw) {
if (!Array.isArray(raw)) {
throw new TypeError('Expected an array, got ' + showValue(raw));
}
var out = [];
for (var i = 0; i < raw.length; ++i) {
try {
out[i] = cleaner(raw[i]);
} catch (error) {
throw locateError(error, '[' + i + ']', 0);
}
}
return out;
};
}
function asObject(shape) {
// The key-value version:
if (typeof shape === 'function') {
return function asObject(raw) {
if (typeof raw !== 'object' || raw == null) {
throw new TypeError('Expected an object, got ' + showValue(raw));
}
var out = {};
var keys = Object.keys(raw);
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (key === '__proto__') continue;
try {
out[key] = shape(raw[key]);
} catch (error) {
throw locateError(error, '[' + JSON.stringify(key) + ']', 0);
}
}
return out;
};
} // The shape-aware version:
var keys = Object.keys(shape);
function bindObjectShape(keepRest) {
return function asObject(raw) {
if (typeof raw !== 'object' || raw == null) {
throw new TypeError('Expected an object, got ' + showValue(raw));
}
var i;
var out = {};
if (keepRest) {
var toCopy = Object.keys(raw);
for (i = 0; i < toCopy.length; ++i) {
var key = toCopy[i];
if (key === '__proto__') continue;
out[key] = raw[key];
}
}
for (i = 0; i < keys.length; ++i) {
var _key = keys[i];
try {
out[_key] = shape[_key](raw[_key]);
} catch (error) {
throw locateError(error, '.' + _key, 0);
}
}
return out;
};
}
var out = bindObjectShape(false);
out.shape = shape;
out.withRest = bindObjectShape(true);
return out;
} // Deprecated:
var asMap = asObject;
function asTuple() {
for (var _len = arguments.length, shape = new Array(_len), _key = 0; _key < _len; _key++) {
shape[_key] = arguments[_key];
}
function asTuple(raw) {
if (!Array.isArray(raw)) {
throw new TypeError('Expected an array, got ' + showValue(raw));
}
var out = [];
for (var i = 0; i < shape.length; ++i) {
try {
out[i] = shape[i](raw[i]);
} catch (error) {
throw locateError(error, "[" + i + "]");
}
}
return out;
}
return asTuple;
}
function asEither() {
for (var _len = arguments.length, shape = new Array(_len), _key = 0; _key < _len; _key++) {
shape[_key] = arguments[_key];
}
return function asEither(raw) {
for (var i = 0; i < shape.length; i++) {
try {
return shape[i](raw);
} catch (error) {
if (i + 1 >= shape.length) throw error;
}
}
};
}
function asMaybe(cleaner, fallback) {
return function asMaybe(raw) {
try {
return cleaner(raw);
} catch (e) {
return typeof fallback === 'function' ? fallback() : fallback;
}
};
}
function asCodec(cleaner, uncleaner) {
return function asCodec(raw) {
return global[uncleaning] > 0 ? uncleaner(raw) : cleaner(raw);
};
}
function uncleaner(cleaner) {
return function uncleaner(raw) {
// We need to set a flag to indicate that we are un-cleaning.
// The flag needs to be in a global location,
// since multiple cleaner implementations should agree
// which mode we are in.
if (global[uncleaning] == null) {
Object.defineProperty(global, uncleaning, {
value: 0,
writable: true
});
}
try {
++global[uncleaning];
return cleaner(raw);
} finally {
--global[uncleaning];
}
};
} // If we cannot find the standardized "globalThis" object,
// fall back on using the "JSON" constructor, which is also well-known.
var global = typeof globalThis === 'object' && globalThis != null ? globalThis : JSON;
/* istanbul ignore next */
var uncleaning = typeof Symbol === 'function' ? Symbol["for"]('uncleaning') : 'uncleaning';
var asDate = asCodec(function asDate(raw) {
if (typeof raw !== 'string' && !(raw instanceof Date)) {
throw new TypeError('Expected a date string, got ' + showValue(raw));
}
var out = new Date(raw);
if (out.toJSON() == null) {
throw new TypeError('Invalid date format, got ' + showValue(raw));
}
return out;
}, function wasDate(clean) {
return clean.toISOString();
});
function asJSON(cleaner) {
return asCodec(function asJSON(raw) {
var value = JSON.parse(asString(raw));
try {
return cleaner(value);
} catch (error) {
throw locateError(error, 'JSON.parse()', 11);
}
}, function wasJSON(clean) {
return JSON.stringify(cleaner(clean));
});
}
export { asArray, asBoolean, asCodec, asDate, asEither, asJSON, asMap, asMaybe, asNone, asNull, asNumber, asObject, asOptional, asString, asTuple, asUndefined, asUnknown, asValue, uncleaner };