UNPKG

cleaners

Version:

Cleans & validates untrusted data, with TypeScript & Flow support

311 lines (253 loc) 7.69 kB
/** * 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 };