UNPKG

elm-decoders

Version:

A powerful, well tested, data decoder for Typescript.

966 lines (830 loc) 25.6 kB
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelperLoose(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } it = o[Symbol.iterator](); return it.next.bind(it); } var Result = /*#__PURE__*/ /** @class */ function () { var Result = function Result(value) { var _this = this; this.map = function (f) { switch (_this.get.type) { case 'OK': return new Result({ type: 'OK', value: f(_this.get.value) }); case 'FAIL': return new Result({ type: 'FAIL', error: _this.get.error }); } }; this.mapError = function (f) { switch (_this.get.type) { case 'OK': return new Result({ type: 'OK', value: _this.get.value }); case 'FAIL': return new Result({ type: 'FAIL', error: f(_this.get.error) }); } }; this.andThen = function (f) { switch (_this.get.type) { case 'OK': return f(_this.get.value); case 'FAIL': return new Result({ type: 'FAIL', error: _this.get.error }); } }; this.get = value; }; Result.merge = function (values, addErrorIndex) { return values.reduce(function (p, c, i) { switch (p.get.type) { case 'OK': switch (c.get.type) { case 'OK': return Result.ok([].concat(p.get.value, [c.get.value])); case 'FAIL': return Result.fail([addErrorIndex(i, c.get.error)]); } case 'FAIL': switch (c.get.type) { case 'OK': return Result.fail(p.get.error); case 'FAIL': return Result.fail([].concat(p.get.error, [addErrorIndex(i, c.get.error)])); } } }, Result.ok([])); }; Result.ok = function (value) { return new Result({ type: 'OK', value: value }); }; Result.fail = function (error) { return new Result({ type: 'FAIL', error: error }); }; Result.isOk = function (value) { return value.get.type === 'OK'; }; Result.isFail = function (value) { return value.get.type === 'FAIL'; }; return Result; }(); var makeSingleError = function makeSingleError(error, value) { return { error: error, value: value }; }; var formatIndex = function formatIndex(index, error) { var check = function check(de) { return typeof de.error === 'string' ? true : false; }; if (check(error)) return { index: index, error: error.error, value: error.value };else return [index, error]; }; var isDate = function isDate(d) { return !isNaN(d.getDate()); }; var isISO = function isISO(str) { return str.match(/(\d{4})-(\d{2})-(\d{2})/) !== null; }; var isInteger = function isInteger(n) { return Math.floor(n) === n && n !== Infinity; }; var numberReSnippet = '(?:NaN|-?(?:(?:\\d+|\\d*\\.\\d+)(?:[E|e][+|-]?\\d+)?|Infinity))'; var matchOnlyNumberRe = /*#__PURE__*/new RegExp('^(' + numberReSnippet + ')$'); var isStringNumber = function isStringNumber(n) { return n.length !== 0 && n.match(matchOnlyNumberRe) !== null; }; var ValidationFailedError = /*#__PURE__*/function (_Error) { _inheritsLoose(ValidationFailedError, _Error); function ValidationFailedError(error) { var _this; _this = _Error.call(this, "Decoding value failed: " + JSON.stringify(error)) || this; _this.error = error; Object.setPrototypeOf(_assertThisInitialized(_this), ValidationFailedError.prototype); return _this; } return ValidationFailedError; }( /*#__PURE__*/_wrapNativeSuper(Error)); /** * Decode data and check it's validity using Decoder. Useful when you want to * check that data from clients or other outgoing sources is valid. * * To create a decoder, use one of the primitive decoders provided as a static method. * Then call it's deocde method on the data you want to decode. * * ``` * const idDecoder<{id: string}> = Decoder.object({id: Decoder.string}) * * idDecoder.run("2913088") // Failure, must have a field id. * * const result = idDecoder.run({id: 2913088}) // OK * * // To access the result value * switch(result.type) { * case "OK": * doThingWithId(result.value) * case "FAIL": * // Or if it fails you can find the reason by accessing error * throw new Error(result.error) * } * ``` * */ var Decoder = /*#__PURE__*/ /** @class */ function () { var Decoder = function Decoder(decoder) { var _this2 = this; /** * Transform a decoder from T to S. * * Example: * ``` * const setDecoder: Decoder<Set<number>> = * Decoder.array(Decoder.number).map(numberArray => new Set(numberArray)) * ``` */ this.map = function (mapFunction) { return new Decoder(function (data) { return _this2.decoder(data).map(function (res) { return mapFunction(res); }); }); }; /** * Sets a default value to the decoder if it fails. * * ``` * const nrDecoder = Decoder.number.default(0) * * nrDecoder.run(5) // OK 5 * nrDecoder.run('hi') // OK 0 * ``` */ this["default"] = function (value) { return new Decoder(function (data) { var result = _this2.decoder(data); if (Result.isOk(result)) return result;else return Result.ok(value); }); }; /** * Run a decoder on data. The result is a [discriminated union](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions). * * * Example: * ``` * const userCredentials: Decoder<Credentials> = Decoder.object({...}) * //... Somewhere else * const result = userCredentials.run(request.query) * switch(result.type) { * case "OK": * login(result.value) * case "FAIL": * throw new Error(result.error) * } * ``` */ this.run = function (data) { return _this2.decoder(data).get; }; /** * Run a decoder on data. It will either succeed and yield the value or throw * an ValidationFailed error */ this.guard = function (data) { var result = _this2.decoder(data).get; if (result.type === 'OK') return result.value;else throw new ValidationFailedError(result.error); }; /** * Create decoders that is dependent on previous results. * * Example: * ``` * const version = Decoder.field('version, Decoder.number) * const api = ({ version }: { version: number }): Decoder<{...}> => { * switch (version) { * case 0: * return myFirstDecoder; * case 1: * return mySecondDecoder; * default: * return Decoder.fail('Version ${version} not supported') * } * }; * const versionedApi = version.then(api); * ``` */ this.then = function (dependentDecoder) { return new Decoder(function (data) { var result = _this2.decoder(data); switch (result.get.type) { case 'OK': return dependentDecoder(result.get.value).decoder(data); case 'FAIL': return Result.fail(result.get.error); } }); }; /** * Add an extra predicate to the decoder. Optionally add a failure message * that overrides the earlier failure message. * * Example: * ``` * const naturalNumber = Decoder.number.satisfy({ * predicate: n => n>0 * failureMessage: `Not a natural number` * }) * naturalNumber.run(5) // OK, 5 * naturalNumber.run(-1) // FAIL, Not a natural number * ``` */ this.satisfy = function (_ref) { var predicate = _ref.predicate, failureMessage = _ref.failureMessage; return _this2.then(function (value) { if (predicate(value)) { return Decoder.ok(value); } else { var message = failureMessage ? failureMessage : "Not fulfilled predicate"; return Decoder.fail(message); } }); }; /** * run a decoder on each item in an array, collecting the amount of successful and failed decodings. * * ``` * example: * * Decoder.number.sequence([1,3,4,"hello"]) // => {successful: [1,2,3], failed: ["hello"]} * ``` */ this.sequence = function (array) { var failed = []; var successful = []; for (var _iterator = _createForOfIteratorHelperLoose(array), _step; !(_step = _iterator()).done;) { var item = _step.value; var result = _this2.run(item); switch (result.type) { case 'OK': successful.push(result.value); break; case 'FAIL': failed.push(item); } } return { failed: failed, successful: successful }; }; this.decoder = decoder; }; Decoder.createOneOf = function (decoders) { return new Decoder(function (data) { var next; var errors = []; for (var _iterator2 = _createForOfIteratorHelperLoose(decoders), _step2; !(_step2 = _iterator2()).done;) { next = _step2.value; var result = next.decoder(data).get; switch (result.type) { case 'OK': return Result.ok(result.value); case 'FAIL': if (Array.isArray(result.error) && Array.isArray(errors)) errors = [].concat(errors, result.error);else errors.push(result.error); } } return Result.fail(errors); }); }; /** * Attempt multiple decoders in order until one succeeds. The type signature is informally: * ``` * oneOf = (decoders: [Decoder<A>, Decoder<B>, ...]) => Decoder<A | B | ...> * ``` * * Example: * ``` * const enums: Decoder<"Jack" | "Sofia"> = Decoder.oneOf([ * Decoder.literalString("Jack"), * Decoder.literalString("Sofia") * ]) * enums.run("Jack") // OK * enums.run("Sofia") // OK * enums.run("Josefine") // Fail * ``` */ Decoder.oneOf = function (decoders) { if (decoders.length === 0) return Decoder.ok; return Decoder.createOneOf(decoders); }; /** * A decoder for numbers. * * Example: * ``` * Decoder.number.run(5) // OK * Decoder.number.run('5') // OK * Decoder.number.run('hi') // FAIL * ``` */ Decoder.number = /*#__PURE__*/new Decoder(function (data) { switch (typeof data) { case 'number': if (!isNaN(data)) return Result.ok(data); return Result.fail(makeSingleError('Not a number', data)); case 'string': if (isStringNumber(data)) return Result.ok(parseFloat(data)); return Result.fail(makeSingleError('Not a number', data)); default: return Result.fail(makeSingleError('Not a number', data)); } }); /** * A decoder for iso dates. Use `Decoder.date` to also support timestamps. * * Example: * ``` * Decoder.date.run(new Date()) // OK * Decoder.date.run("abra") // FAIL * Decoder.date.run("2020-01-13T18:27:35.817Z") // OK * Decoder.date.run(123) // FAIL * Decoder.date.run("Mon, 13 Jan 2020 18:28:05 GMT") // FAIL, format is not supported * ``` */ Decoder.isoDate = /*#__PURE__*/new Decoder(function (data) { switch (Object.prototype.toString.call(data)) { case '[object Date]': if (isDate(data)) return Result.ok(data); return Result.fail(makeSingleError('Badly formatted date object', data)); case '[object String]': return isISO(data) ? Result.ok(new Date(data)) : Result.fail(makeSingleError("Not a ISO date", data)); default: return Result.fail(makeSingleError("Not a ISO date", data)); } }); /** * A decoder for timestamps. * * Example: * ``` * Decoder.date.run(123) // OK (Timestamp) * Decoder.date.run(new Date()) // FAIL * Decoder.date.run("abra") // FAIL * Decoder.date.run("2020-01-13T18:27:35.817Z") // FAIL * Decoder.date.run("Mon, 13 Jan 2020 18:28:05 GMT") // FAIL * ``` */ Decoder.timestamp = /*#__PURE__*/Decoder.number.satisfy({ predicate: isInteger, failureMessage: 'Not a timestamp' }); /** * A decoder for dates. Decoding UTC time that is formatted using * `toUTCString()` is not supported; Javascript's date parser parses UTC strings * wrong. * * Example: * ``` * Decoder.date.run(123) // OK (Timestamp) * Decoder.date.run(new Date()) // OK * Decoder.date.run("abra") // FAIL * Decoder.date.run("2020-01-13T18:27:35.817Z") // OK * Decoder.date.run("Mon, 13 Jan 2020 18:28:05 GMT") // FAIL, format is not supported * ``` */ Decoder.date = /*#__PURE__*/Decoder.oneOf([/*#__PURE__*/Decoder.timestamp.map(function (n) { return new Date(n); }), Decoder.isoDate]); /** * A decoder that accepts undefined. * * Example: * ``` * Decoder.undefined.run(null) // FAIL * Decoder.undefined.run(5) // FAIL * Decoder.undefined.run(undefined) // OK *``` */ Decoder.undefined = /*#__PURE__*/new Decoder(function (data) { return data === undefined ? Result.ok(data) : Result.fail(makeSingleError('Not undefined', data)); }); /** * A decoder that accepts null. * * Example: * ``` * Decoder.null.run(undefined) // FAIL * Decoder.null.run(5) // FAIL * Decoder.null.run(null) // OK *``` */ Decoder["null"] = /*#__PURE__*/new Decoder(function (data) { return data === null ? Result.ok(data) : Result.fail(makeSingleError('Not null', data)); }); /** * A decoder that accepts a Buffer. * * Example: * ``` * Decoder.null.run(undefined) // FAIL * Decoder.null.run(5) // FAIL * Decoder.null.run(Buffer.from('Hello world')) // OK * Decoder.null.run(<Buffer 68 65 6c 6c 6f>) // OK *``` */ Decoder.buffer = /*#__PURE__*/new Decoder(function (data) { return Buffer.isBuffer(data) ? Result.ok(data) : Result.fail(makeSingleError('Not Buffer', data)); }); /** * Decodes a string. * * Example: * ``` * Decoder.string.run('hi') // OK * Decoder.string.run(5) // Fail * ``` */ Decoder.string = /*#__PURE__*/new Decoder(function (data) { return typeof data === 'string' ? Result.ok(data) : Result.fail(makeSingleError('Not a string', data)); }); /** * Decodes the exact string and sets it to a string literal type. Useful for * parsing unions. * * Example: * ``` * const jackOrSofia: Decoder<'Jack' | 'Sofia'> = Decoder.oneOf([ * Decoder.literalString('Jack'), * Decoder.literalString('Sofia') * ]) * jackOrSofia.run('Jack') // OK * jackOrSofia.run('Josephine') // FAIL * ``` */ Decoder.literalString = function (str) { return Decoder.string.then(function (incomingStr) { return incomingStr === str ? Decoder.ok(str) : Decoder.fail("Not " + str); }); }; /** * Takes a decoder and returns an optional decoder. * * Example: * ``` * const optionalNumber = Decoder.optional(Decoder.number) * optionalNumber.run(5) //OK * optionalNumber.run(undefined) //OK * optionalNumber.run(null) //OK * optionalNumber.run('hi') //FAIL * ``` */ Decoder.optional = function (decoder) { return Decoder.oneOf([Decoder.undefined, Decoder["null"].map(function (_) { return undefined; }), decoder]); }; /** * Create a decoder that always fails with a message. */ Decoder.fail = function (message) { return new Decoder(function () { return Result.fail({ error: message }); }); }; /** * Create a decoder that always suceeds and returns T. */ Decoder.ok = function (value) { return new Decoder(function () { return Result.ok(value); }); }; /** * Decodes the exact number and sets it to a number literal type. * * Example: * ``` * const versionDecoder: Decoder<1 | 2> = Decoder.oneOf([ * Decoder.literalNumber(1), * Decoder.literalNumber(2) * ]) * * versionDecoder.run(1) // OK * versionDecoder.run(3) // FAIL * ``` */ Decoder.literalNumber = function (number) { return Decoder.number.then(function (incomingNumber) { return incomingNumber === number ? Decoder.ok(number) : Decoder.fail("Not " + number); }); }; /** * Create an array decoder given a decoder for the elements. * * Example: * ``` * Decoder.array(Decoder.string).run(['hello','world']) // OK * Decoder.array(Decoder.string).run(5) // Fail * ``` */ Decoder.array = function (decoder) { return new Decoder(function (data) { if (Array.isArray(data)) { return Result.merge(data.map(function (item) { return decoder.decoder(item); }), function (index, e) { return formatIndex(index, e); }); } else return Result.fail(makeSingleError('Not an array', data)); }); }; /** * Create a decoder for booleans. * * Example: * ``` * Decoder.boolean.run(true) // succeeds * Decoder.boolean.run('false') // succeeds * Decoder.boolean.run(1) // fails * ``` */ Decoder["boolean"] = /*#__PURE__*/new Decoder(function (data) { switch (typeof data) { case 'boolean': return Result.ok(data); case 'string': switch (data) { case 'true': return Result.ok(true); case 'false': return Result.ok(false); default: return Result.fail(makeSingleError('Not a boolean', data)); } default: { return Result.fail(makeSingleError('Not a boolean', data)); } } }); /** * Decode the value of a specific key in an object using a given decoder. * * Example: * ``` * const versionDecoder = Decoder.field("version", Decoder.number) * * versionDecoder.run({version: 5}) // OK, 5 * versionDecoder.run({name: "hi"}) // fail * ``` * */ Decoder.field = function (key, decoder) { return new Decoder(function (data) { if (typeof data === 'object' && data !== null) { return decoder.decoder(data[key]).mapError(function (e) { var _ref2; return _ref2 = {}, _ref2[key] = e, _ref2; }); } else { return Result.fail(makeSingleError('Not an object', data)); } }); }; /** * A decoder that accepts anything. */ Decoder.any = /*#__PURE__*/new Decoder(function (data) { return Result.ok(data); }); /** * Decode values of an object where the keys are unknown. * * Example: * ``` * const userDecoder = Decoder.object({age: Decoder.number}) * const usersDecoder = Decoder.dict(userDecoder) * * usersDecoder.run({emelie: {age: 32}, bob: {age: 50}}) // OK, {emelie: {age: 32}, bob: {age: 50}} * usersDecoder.run({name: 'emelie', age: 32}) // fail * ``` * */ Decoder.dict = function (decoder) { return new Decoder(function (data) { if (typeof data === 'object' && data !== null) { var decoded = {}; var errors = {}; for (var key in data) { var result = decoder.run(data[key]); switch (result.type) { case 'OK': decoded[key] = result.value; break; case 'FAIL': errors[key] = result.error; break; } } if (Object.keys(errors).length === 0) return Result.ok(decoded); return Result.fail(errors); } else { return Result.fail(makeSingleError('Not an object', data)); } }); }; /** * Create a decoder for a type T. * * Argument "object" is a [Mapped * type](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types), * an object containing only decoders for each field. * ```typescript * {name: Decoder.string} // Ok parameter * {name: Decoder.string, email: 'email@email'} // Type error, email must be decoder * ``` * * Example: * ``` * interface User { * name: string * email: string * } * * // typechecks * const decodeUser: Decoder<User> = Decoder.object({name: Decoder.string, email: Decoder.email}) * decodeUser.run({name: "Jenny", email: "fakemail@fake.com"}) // OK * decodeUser.run({nm: "Bad"}) // FAIL * * // will not typecheck, object must have the same field names as user and * // only contain decoders. * const decodeUser: Decoder<User> = Decoder.object({nem: 'Hi'}) * ``` * */ Decoder.object = function (object) { return new Decoder(function (data) { if (typeof data === 'object' && data !== null) { var obj = {}; var errors = {}; var key; for (key in object) { var result = object[key].decoder(data[key]).get; switch (result.type) { case 'OK': obj[key] = result.value; break; case 'FAIL': errors[key] = result.error; break; } } if (Object.keys(errors).length === 0) return Result.ok(obj); return Result.fail(errors); } else { return Result.fail(makeSingleError('Not an object', data)); } }); }; return Decoder; }(); export { Decoder, ValidationFailedError }; //# sourceMappingURL=elm-decoders.esm.js.map