@mojotech/json-type-validation
Version:
runtime type checking and validation of untyped JSON data
604 lines • 23.2 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var Result = require("./result");
var isEqual = require('lodash.isequal'); // this syntax avoids TS1192
/**
* Type guard for `DecoderError`. One use case of the type guard is in the
* `catch` of a promise. Typescript types the error argument of `catch` as
* `any`, so when dealing with a decoder as a promise you may need to
* distinguish between a `DecoderError` and an error string.
*/
exports.isDecoderError = function (a) {
return a.kind === 'DecoderError' && typeof a.at === 'string' && typeof a.message === 'string';
};
/*
* Helpers
*/
var isJsonArray = function (json) { return Array.isArray(json); };
var isJsonObject = function (json) {
return typeof json === 'object' && json !== null && !isJsonArray(json);
};
var typeString = function (json) {
switch (typeof json) {
case 'string':
return 'a string';
case 'number':
return 'a number';
case 'boolean':
return 'a boolean';
case 'undefined':
return 'undefined';
case 'object':
if (json instanceof Array) {
return 'an array';
}
else if (json === null) {
return 'null';
}
else {
return 'an object';
}
default:
return JSON.stringify(json);
}
};
var expectedGot = function (expected, got) {
return "expected " + expected + ", got " + typeString(got);
};
var printPath = function (paths) {
return paths.map(function (path) { return (typeof path === 'string' ? "." + path : "[" + path + "]"); }).join('');
};
var prependAt = function (newAt, _a) {
var at = _a.at, rest = __rest(_a, ["at"]);
return (__assign({ at: newAt + (at || '') }, rest));
};
/**
* Decoders transform json objects with unknown structure into known and
* verified forms. You can create objects of type `Decoder<A>` with either the
* primitive decoder functions, such as `boolean()` and `string()`, or by
* applying higher-order decoders to the primitives, such as `array(boolean())`
* or `dict(string())`.
*
* Each of the decoder functions are available both as a static method on
* `Decoder` and as a function alias -- for example the string decoder is
* defined at `Decoder.string()`, but is also aliased to `string()`. Using the
* function aliases exported with the library is recommended.
*
* `Decoder` exposes a number of 'run' methods, which all decode json in the
* same way, but communicate success and failure in different ways. The `map`
* and `andThen` methods modify decoders without having to call a 'run' method.
*
* Alternatively, the main decoder `run()` method returns an object of type
* `Result<A, DecoderError>`. This library provides a number of helper
* functions for dealing with the `Result` type, so you can do all the same
* things with a `Result` as with the decoder methods.
*/
var Decoder = /** @class */ (function () {
/**
* The Decoder class constructor is kept private to separate the internal
* `decode` function from the external `run` function. The distinction
* between the two functions is that `decode` returns a
* `Partial<DecoderError>` on failure, which contains an unfinished error
* report. When `run` is called on a decoder, the relevant series of `decode`
* calls is made, and then on failure the resulting `Partial<DecoderError>`
* is turned into a `DecoderError` by filling in the missing information.
*
* While hiding the constructor may seem restrictive, leveraging the
* provided decoder combinators and helper functions such as
* `andThen` and `map` should be enough to build specialized decoders as
* needed.
*/
function Decoder(decode) {
var _this = this;
this.decode = decode;
/**
* Run the decoder and return a `Result` with either the decoded value or a
* `DecoderError` containing the json input, the location of the error, and
* the error message.
*
* Examples:
* ```
* number().run(12)
* // => {ok: true, result: 12}
*
* string().run(9001)
* // =>
* // {
* // ok: false,
* // error: {
* // kind: 'DecoderError',
* // input: 9001,
* // at: 'input',
* // message: 'expected a string, got 9001'
* // }
* // }
* ```
*/
this.run = function (json) {
return Result.mapError(function (error) { return ({
kind: 'DecoderError',
input: json,
at: 'input' + (error.at || ''),
message: error.message || ''
}); }, _this.decode(json));
};
/**
* Run the decoder as a `Promise`.
*/
this.runPromise = function (json) { return Result.asPromise(_this.run(json)); };
/**
* Run the decoder and return the value on success, or throw an exception
* with a formatted error string.
*/
this.runWithException = function (json) { return Result.withException(_this.run(json)); };
/**
* Construct a new decoder that applies a transformation to the decoded
* result. If the decoder succeeds then `f` will be applied to the value. If
* it fails the error will propagated through.
*
* Example:
* ```
* number().map(x => x * 5).run(10)
* // => {ok: true, result: 50}
* ```
*/
this.map = function (f) {
return new Decoder(function (json) { return Result.map(f, _this.decode(json)); });
};
/**
* Chain together a sequence of decoders. The first decoder will run, and
* then the function will determine what decoder to run second. If the result
* of the first decoder succeeds then `f` will be applied to the decoded
* value. If it fails the error will propagate through.
*
* This is a very powerful method -- it can act as both the `map` and `where`
* methods, can improve error messages for edge cases, and can be used to
* make a decoder for custom types.
*
* Example of adding an error message:
* ```
* const versionDecoder = valueAt(['version'], number());
* const infoDecoder3 = object({a: boolean()});
*
* const decoder = versionDecoder.andThen(version => {
* switch (version) {
* case 3:
* return infoDecoder3;
* default:
* return fail(`Unable to decode info, version ${version} is not supported.`);
* }
* });
*
* decoder.run({version: 3, a: true})
* // => {ok: true, result: {a: true}}
*
* decoder.run({version: 5, x: 'abc'})
* // =>
* // {
* // ok: false,
* // error: {... message: 'Unable to decode info, version 5 is not supported.'}
* // }
* ```
*
* Example of decoding a custom type:
* ```
* // nominal type for arrays with a length of at least one
* type NonEmptyArray<T> = T[] & { __nonEmptyArrayBrand__: void };
*
* const nonEmptyArrayDecoder = <T>(values: Decoder<T>): Decoder<NonEmptyArray<T>> =>
* array(values).andThen(arr =>
* arr.length > 0
* ? succeed(createNonEmptyArray(arr))
* : fail(`expected a non-empty array, got an empty array`)
* );
* ```
*/
this.andThen = function (f) {
return new Decoder(function (json) {
return Result.andThen(function (value) { return f(value).decode(json); }, _this.decode(json));
});
};
/**
* Add constraints to a decoder _without_ changing the resulting type. The
* `test` argument is a predicate function which returns true for valid
* inputs. When `test` fails on an input, the decoder fails with the given
* `errorMessage`.
*
* ```
* const chars = (length: number): Decoder<string> =>
* string().where(
* (s: string) => s.length === length,
* `expected a string of length ${length}`
* );
*
* chars(5).run('12345')
* // => {ok: true, result: '12345'}
*
* chars(2).run('HELLO')
* // => {ok: false, error: {... message: 'expected a string of length 2'}}
*
* chars(12).run(true)
* // => {ok: false, error: {... message: 'expected a string, got a boolean'}}
* ```
*/
this.where = function (test, errorMessage) {
return _this.andThen(function (value) { return (test(value) ? Decoder.succeed(value) : Decoder.fail(errorMessage)); });
};
}
/**
* Decoder primitive that validates strings, and fails on all other input.
*/
Decoder.string = function () {
return new Decoder(function (json) {
return typeof json === 'string'
? Result.ok(json)
: Result.err({ message: expectedGot('a string', json) });
});
};
/**
* Decoder primitive that validates numbers, and fails on all other input.
*/
Decoder.number = function () {
return new Decoder(function (json) {
return typeof json === 'number'
? Result.ok(json)
: Result.err({ message: expectedGot('a number', json) });
});
};
/**
* Decoder primitive that validates booleans, and fails on all other input.
*/
Decoder.boolean = function () {
return new Decoder(function (json) {
return typeof json === 'boolean'
? Result.ok(json)
: Result.err({ message: expectedGot('a boolean', json) });
});
};
Decoder.constant = function (value) {
return new Decoder(function (json) {
return isEqual(json, value)
? Result.ok(value)
: Result.err({ message: "expected " + JSON.stringify(value) + ", got " + JSON.stringify(json) });
});
};
Decoder.object = function (decoders) {
return new Decoder(function (json) {
if (isJsonObject(json) && decoders) {
var obj = {};
for (var key in decoders) {
if (decoders.hasOwnProperty(key)) {
var r = decoders[key].decode(json[key]);
if (r.ok === true) {
// tslint:disable-next-line:strict-type-predicates
if (r.result !== undefined) {
obj[key] = r.result;
}
}
else if (json[key] === undefined) {
return Result.err({ message: "the key '" + key + "' is required but was not present" });
}
else {
return Result.err(prependAt("." + key, r.error));
}
}
}
return Result.ok(obj);
}
else if (isJsonObject(json)) {
return Result.ok(json);
}
else {
return Result.err({ message: expectedGot('an object', json) });
}
});
};
Decoder.array = function (decoder) {
return new Decoder(function (json) {
if (isJsonArray(json) && decoder) {
var decodeValue_1 = function (v, i) {
return Result.mapError(function (err) { return prependAt("[" + i + "]", err); }, decoder.decode(v));
};
return json.reduce(function (acc, v, i) {
return Result.map2(function (arr, result) { return arr.concat([result]); }, acc, decodeValue_1(v, i));
}, Result.ok([]));
}
else if (isJsonArray(json)) {
return Result.ok(json);
}
else {
return Result.err({ message: expectedGot('an array', json) });
}
});
};
Decoder.tuple = function (decoders) {
return new Decoder(function (json) {
if (isJsonArray(json)) {
if (json.length !== decoders.length) {
return Result.err({
message: "expected a tuple of length " + decoders.length + ", got one of length " + json.length
});
}
var result = [];
for (var i = 0; i < decoders.length; i++) {
var nth = decoders[i].decode(json[i]);
if (nth.ok) {
result[i] = nth.result;
}
else {
return Result.err(prependAt("[" + i + "]", nth.error));
}
}
return Result.ok(result);
}
else {
return Result.err({ message: expectedGot("a tuple of length " + decoders.length, json) });
}
});
};
Decoder.union = function (ad, bd) {
var decoders = [];
for (var _i = 2; _i < arguments.length; _i++) {
decoders[_i - 2] = arguments[_i];
}
return Decoder.oneOf.apply(Decoder, [ad, bd].concat(decoders));
};
Decoder.intersection = function (ad, bd) {
var ds = [];
for (var _i = 2; _i < arguments.length; _i++) {
ds[_i - 2] = arguments[_i];
}
return new Decoder(function (json) {
return [ad, bd].concat(ds).reduce(function (acc, decoder) { return Result.map2(Object.assign, acc, decoder.decode(json)); }, Result.ok({}));
});
};
/**
* Escape hatch to bypass validation. Always succeeds and types the result as
* `any`. Useful for defining decoders incrementally, particularly for
* complex objects.
*
* Example:
* ```
* interface User {
* name: string;
* complexUserData: ComplexType;
* }
*
* const userDecoder: Decoder<User> = object({
* name: string(),
* complexUserData: anyJson()
* });
* ```
*/
Decoder.anyJson = function () { return new Decoder(function (json) { return Result.ok(json); }); };
/**
* Decoder identity function which always succeeds and types the result as
* `unknown`.
*/
Decoder.unknownJson = function () {
return new Decoder(function (json) { return Result.ok(json); });
};
/**
* Decoder for json objects where the keys are unknown strings, but the values
* should all be of the same type.
*
* Example:
* ```
* dict(number()).run({chocolate: 12, vanilla: 10, mint: 37});
* // => {ok: true, result: {chocolate: 12, vanilla: 10, mint: 37}}
* ```
*/
Decoder.dict = function (decoder) {
return new Decoder(function (json) {
if (isJsonObject(json)) {
var obj = {};
for (var key in json) {
if (json.hasOwnProperty(key)) {
var r = decoder.decode(json[key]);
if (r.ok === true) {
obj[key] = r.result;
}
else {
return Result.err(prependAt("." + key, r.error));
}
}
}
return Result.ok(obj);
}
else {
return Result.err({ message: expectedGot('an object', json) });
}
});
};
/**
* Decoder for values that may be `undefined`. This is primarily helpful for
* decoding interfaces with optional fields.
*
* Example:
* ```
* interface User {
* id: number;
* isOwner?: boolean;
* }
*
* const decoder: Decoder<User> = object({
* id: number(),
* isOwner: optional(boolean())
* });
* ```
*/
Decoder.optional = function (decoder) {
return new Decoder(function (json) { return (json === undefined ? Result.ok(undefined) : decoder.decode(json)); });
};
/**
* Decoder that attempts to run each decoder in `decoders` and either succeeds
* with the first successful decoder, or fails after all decoders have failed.
*
* Note that `oneOf` expects the decoders to all have the same return type,
* while `union` creates a decoder for the union type of all the input
* decoders.
*
* Examples:
* ```
* oneOf(string(), number().map(String))
* oneOf(constant('start'), constant('stop'), succeed('unknown'))
* ```
*/
Decoder.oneOf = function () {
var decoders = [];
for (var _i = 0; _i < arguments.length; _i++) {
decoders[_i] = arguments[_i];
}
return new Decoder(function (json) {
var errors = [];
for (var i = 0; i < decoders.length; i++) {
var r = decoders[i].decode(json);
if (r.ok === true) {
return r;
}
else {
errors[i] = r.error;
}
}
var errorsList = errors
.map(function (error) { return "at error" + (error.at || '') + ": " + error.message; })
.join('", "');
return Result.err({
message: "expected a value matching one of the decoders, got the errors [\"" + errorsList + "\"]"
});
});
};
/**
* Decoder that always succeeds with either the decoded value, or a fallback
* default value.
*/
Decoder.withDefault = function (defaultValue, decoder) {
return new Decoder(function (json) {
return Result.ok(Result.withDefault(defaultValue, decoder.decode(json)));
});
};
/**
* Decoder that pulls a specific field out of a json structure, instead of
* decoding and returning the full structure. The `paths` array describes the
* object keys and array indices to traverse, so that values can be pulled out
* of a nested structure.
*
* Example:
* ```
* const decoder = valueAt(['a', 'b', 0], string());
*
* decoder.run({a: {b: ['surprise!']}})
* // => {ok: true, result: 'surprise!'}
*
* decoder.run({a: {x: 'cats'}})
* // => {ok: false, error: {... at: 'input.a.b[0]' message: 'path does not exist'}}
* ```
*
* Note that the `decoder` is ran on the value found at the last key in the
* path, even if the last key is not found. This allows the `optional`
* decoder to succeed when appropriate.
* ```
* const optionalDecoder = valueAt(['a', 'b', 'c'], optional(string()));
*
* optionalDecoder.run({a: {b: {c: 'surprise!'}}})
* // => {ok: true, result: 'surprise!'}
*
* optionalDecoder.run({a: {b: 'cats'}})
* // => {ok: false, error: {... at: 'input.a.b.c' message: 'expected an object, got "cats"'}
*
* optionalDecoder.run({a: {b: {z: 1}}})
* // => {ok: true, result: undefined}
* ```
*/
Decoder.valueAt = function (paths, decoder) {
return new Decoder(function (json) {
var jsonAtPath = json;
for (var i = 0; i < paths.length; i++) {
if (jsonAtPath === undefined) {
return Result.err({
at: printPath(paths.slice(0, i + 1)),
message: 'path does not exist'
});
}
else if (typeof paths[i] === 'string' && !isJsonObject(jsonAtPath)) {
return Result.err({
at: printPath(paths.slice(0, i + 1)),
message: expectedGot('an object', jsonAtPath)
});
}
else if (typeof paths[i] === 'number' && !isJsonArray(jsonAtPath)) {
return Result.err({
at: printPath(paths.slice(0, i + 1)),
message: expectedGot('an array', jsonAtPath)
});
}
else {
jsonAtPath = jsonAtPath[paths[i]];
}
}
return Result.mapError(function (error) {
return jsonAtPath === undefined
? { at: printPath(paths), message: 'path does not exist' }
: prependAt(printPath(paths), error);
}, decoder.decode(jsonAtPath));
});
};
/**
* Decoder that ignores the input json and always succeeds with `fixedValue`.
*/
Decoder.succeed = function (fixedValue) {
return new Decoder(function (json) { return Result.ok(fixedValue); });
};
/**
* Decoder that ignores the input json and always fails with `errorMessage`.
*/
Decoder.fail = function (errorMessage) {
return new Decoder(function (json) { return Result.err({ message: errorMessage }); });
};
/**
* Decoder that allows for validating recursive data structures. Unlike with
* functions, decoders assigned to variables can't reference themselves
* before they are fully defined. We can avoid prematurely referencing the
* decoder by wrapping it in a function that won't be called until use, at
* which point the decoder has been defined.
*
* Example:
* ```
* interface Comment {
* msg: string;
* replies: Comment[];
* }
*
* const decoder: Decoder<Comment> = object({
* msg: string(),
* replies: lazy(() => array(decoder))
* });
* ```
*/
Decoder.lazy = function (mkDecoder) {
return new Decoder(function (json) { return mkDecoder().decode(json); });
};
return Decoder;
}());
exports.Decoder = Decoder;
//# sourceMappingURL=decoder.js.map