elm-decoders
Version:
A powerful, well tested, data decoder for Typescript.
966 lines (830 loc) • 25.6 kB
JavaScript
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