@schroffl/json-mapping
Advanced tools
Comparing version
581
index.d.ts
@@ -0,35 +1,608 @@ | ||
/** | ||
* A library for constructing and running decoders that make sure your data | ||
* looks as expected. | ||
* | ||
* This is a tool that is useful for bringing values safely into your code. | ||
* The source of the data might be something like an HTTP API, user input or | ||
* localStorage, where you can never be sure that you get what you expect. | ||
* | ||
* @packageDocumentation | ||
*/ | ||
/** | ||
* This is intended to be an opaque type. | ||
* You really shouldn't be building `Decoder<T>` values on your own (because | ||
* you can't, but TypeScript doesn't stop you). Use the exposed functions | ||
* for that. | ||
* | ||
* If you ever feel like something is missing, please create an issue in the | ||
* GitHub repository at https://github.com/schroffl/json-mapping | ||
* | ||
* @typeParam T - When running this decoder you get a value of this type | ||
*/ | ||
export type Decoder<T> = { | ||
/** | ||
* @internal | ||
*/ | ||
readonly __opaque_type: 'decoder' | ||
/** | ||
* @internal | ||
*/ | ||
readonly __type: T | ||
} | ||
/** | ||
* Mapped type that creates an object from the given type where every property | ||
* is optional, but wraps the actual type in a Decoder. It's used by | ||
* {@link Decode.object} and {@link Decode.instance}. | ||
* | ||
* @typeParam O - The type to generate the layout for | ||
* | ||
* @example | ||
* If you had a User model like | ||
* ```typescript | ||
* class User { | ||
* id: number; | ||
* name: string; | ||
* children: User[]; | ||
* } | ||
* ``` | ||
* | ||
* the mapped layout would look like this: | ||
* | ||
* ```typescript | ||
* type UserLayout = { | ||
* id?: Decoder<number>, | ||
* name?: Decoder<string>, | ||
* children?: Decoder<User[]>, | ||
* } | ||
* ``` | ||
*/ | ||
export type ObjectLayout<O> = { | ||
[K in keyof O]?: Decoder<O[K]> | ||
} | ||
/** | ||
* This namespace wraps all the decoders exposed by this package. | ||
* It contains primitive decoders like {@link Decode.string} to more | ||
* complicated ones like {@link Decode.map}. | ||
* So basically all the building blocks you need for creating decoders for | ||
* complex data structures. | ||
*/ | ||
export namespace Decode { | ||
/** | ||
* Decode any valid JavaScript Number that is not NaN | ||
* | ||
* @example | ||
* ```typescript | ||
* decode(Decode.number, 42.2) == 42.2 | ||
* decode(Decode.number, 42) == 42 | ||
* decode(Decode.number, NaN) // Throws | ||
* ``` | ||
*/ | ||
export const number: Decoder<number> | ||
/** | ||
* Decode any string value. | ||
* | ||
* @example | ||
* ```typescript | ||
* decode(Decode.string, 'abc') === 'abc' | ||
* decode(Decode.string, 'my-string') === 'my-string' | ||
* decode(Decode.string, 10) // Throws | ||
* ``` | ||
*/ | ||
export const string: Decoder<string> | ||
/** | ||
* Decode an integer. Floating point values are not accepted. | ||
* | ||
* @example | ||
* ```typescript | ||
* decode(Decode.number, 42) == 42 | ||
* decode(Decode.number, 42.2) // Throws | ||
* decode(Decode.number, NaN) // Throws | ||
* ``` | ||
*/ | ||
export const integer: Decoder<number> | ||
/** | ||
* Decode either `true` or `false`. Nothing else. | ||
* | ||
* @example | ||
* ```typescript | ||
* decode(Decode.bool, true) === true | ||
* decode(Decode.bool, false) === false | ||
* decode(Decode.bool, undefined) // Throws | ||
* ``` | ||
*/ | ||
export const bool: Decoder<boolean> | ||
/** | ||
* Decode the value as-is. You probably shouldn't use this, because there's | ||
* a high chance you're abusing it as an escape-hatch. | ||
* However, it has a valid use case for building custom decoders with the | ||
* help of {@link Decode.andThen}. If you do that, please make sure that you keep | ||
* everything safe. | ||
* | ||
* @example | ||
* ```typescript | ||
* decode(Decode.unknown, true) === true | ||
* decode(Decode.unknown, undefined) === undefined | ||
* decode(Decode.unknown, NaN) | ||
* | ||
* // This decoder really blindly passes on the value | ||
* const symbol = Symbol('my-symbol'); | ||
* decode(Decode.unknown, symbol) === symbol | ||
* ``` | ||
*/ | ||
export const unknown: Decoder<unknown> | ||
export type ObjectLayout<O> = { | ||
[K in keyof O]?: Decoder<O[K]> | ||
} | ||
/** | ||
* Decode the value to an object. This implies nothing about the value that | ||
* is being decoded, which can be anything. | ||
* You could take a "plain" integer and "lift" it into an object. | ||
* | ||
* @example | ||
* ```typescript | ||
* const decoder = Decode.object({ | ||
* value: Decode.integer, | ||
* }); | ||
* | ||
* decode(decoder, 42) // { value: 42 } | ||
* decode(decoder, 100) // { value: 100 } | ||
* decode(decoder, '100') // Fails | ||
* ``` | ||
* | ||
* @param layout - Properties you want the object to have | ||
*/ | ||
export function object<O>(layout: ObjectLayout<O>) : Decoder<O> | ||
export function object<O>(layout: ObjectLayout<O>) : Decoder<O> | ||
/** | ||
* Decode an object with arbitrary keys and values of the same type T. | ||
* | ||
* @param child - Decoder to use for values | ||
* | ||
* @example | ||
* ```typescript | ||
* const raw = { en: 'Bread', fr: 'Pain', it: 'Pane' }; | ||
* const decoder = Decode.dict(Decode.string); | ||
* | ||
* decode(decoder, raw); // Works | ||
* | ||
* decode(decoder, { en: 128 }); // Fails | ||
* ``` | ||
*/ | ||
export function dict<T>(child: Decoder<T>) : Decoder<{ [key: string]: T }> | ||
/** | ||
* This is mostly equivalent to {@link Decode.object}, but it creates an | ||
* instance of the given class first. You should only use this for simple | ||
* classes that don't have a complex constructor. Use `map` or `andThen` | ||
* for complicated cases. | ||
* | ||
* @example | ||
* If you had the following User model in your application | ||
* | ||
* ```typescript | ||
* class User { | ||
* id!: number; | ||
* name!: string; | ||
* | ||
* get initials(): string { | ||
* const parts = this.name.split(' '); | ||
* return parts[0][0] + parts[1][0]; | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* your instance decoder would look like this. | ||
* | ||
* ```typescript | ||
* const UserDecoder = Decode.instance(User, { | ||
* id: Decode.field('id', Decode.integer), | ||
* name: Decode.field('name', Decode.string), | ||
* }); | ||
* ``` | ||
* | ||
* Running this decoder on raw values will ensure that you | ||
* always have an actual instance of your User class at hand. | ||
* | ||
* ```typescript | ||
* const kobe = decode(UserDecoder, { id: 3, name: 'Kobe Bryant' }); | ||
* console.assert(kobe.id === 3) | ||
* console.assert(kobe.name === 'Kobe Bryant') | ||
* console.assert(kobe.initials === 'KB') | ||
* ``` | ||
* | ||
* @param ctor - The class you want to construct an instance of | ||
* @param layout - Properties you want to set on the instance | ||
* | ||
* @see object | ||
* @see ObjectLayout | ||
*/ | ||
export function instance<O>(ctor: new () => O, layout: ObjectLayout<O>) : Decoder<O> | ||
/** | ||
* Access the given property of an object. | ||
* | ||
* @param name - Name of the property | ||
* @param child - The decoder to run on the value of that property | ||
* | ||
* @example | ||
* ```typescript | ||
* const decoder = Decode.field('value', Decode.integer); | ||
* | ||
* decode(decoder, { value: 42 }) === 42 | ||
* decode(decoder, {}) // Fails | ||
* decode(decoder, 42) // Fails | ||
* ``` | ||
*/ | ||
export function field<T>(name: string, child: Decoder<T>) : Decoder<T> | ||
/** | ||
* It's basically the same as {@link Decode.field}, but makes it easier | ||
* to define deep property paths. | ||
* Instead of `Decode.field('outer', Decode.field('inner', Decode.string))` | ||
* you can use `Decode.at(['outer', 'inner'], Decode.string)` | ||
* | ||
* @param path - The property path to follow. The first name is the | ||
* outer-most field | ||
* @param child - The decoder to run on that field | ||
* | ||
* @example | ||
* When you want to access the `name` field in an object like this | ||
* ```json | ||
* { | ||
* "data": { | ||
* "outer": { | ||
* "inner": { | ||
* "name": "Kobe Bryant", | ||
* }, | ||
* }, | ||
* }, | ||
* } | ||
* ``` | ||
* | ||
* you would have to chain quite a few {@link Decode.field | field} | ||
* decoders, which is annoying. | ||
* | ||
* ```typescript | ||
* const decoder = Decode.field( | ||
* 'data', | ||
* Decode.field( | ||
* 'outer', | ||
* Decode.field( | ||
* 'inner', | ||
* Decode.field( | ||
* 'name', | ||
* Decode.string, | ||
* ), | ||
* ), | ||
* ), | ||
* ); | ||
* | ||
* decode(decoder, raw) === 'Kobe Bryant' | ||
* ``` | ||
* | ||
* {@link Decode.at} allows us to be more concise. | ||
* | ||
* ```typescript | ||
* const short = Decode.at(['data', 'outer', 'inner', 'name'], Decode.string); | ||
* decode(short, raw) === 'Kobe Bryant' | ||
* ``` | ||
*/ | ||
export function at<T>(path: string[], child: Decoder<T>) : Decoder<T> | ||
/** | ||
* Make a decoder that can be used for decoding arrays, where | ||
* every value is run through the given child decoder. | ||
* | ||
* @param child - Decoder for array items | ||
* | ||
* @example | ||
* Suppose we have a decoder for Users | ||
* ```typescript | ||
* class User {} | ||
* | ||
* const user_decoder = Decode.instance(User, { | ||
* id: Decode.field('id', Decode.integer), | ||
* name: Decode.field('name', Decode.string), | ||
* }); | ||
* ``` | ||
* | ||
* Using {@link Decode.many} we can easily build a decoder for a list of users: | ||
* | ||
* ```typescript | ||
* const decoder = Decode.many(user_decoder); | ||
* | ||
* decode(decoder, [ {id: 1, name: 'Jeff'}, {id: 2, name: 'Jake'} ]); | ||
* ``` | ||
* | ||
* @returns Decoder for an array of things | ||
*/ | ||
export function many<T>(child: Decoder<T>) : Decoder<T[]> | ||
/** | ||
* Take the value decoded by the given decoder and do something | ||
* with it. | ||
* | ||
* @param fn - Your mapping function | ||
* @param child - The decoder to run before calling your function | ||
* | ||
* @typeParam A - Input to the mapping function | ||
* @typeParam B - The mapping function returns a value of this type | ||
* | ||
* @example | ||
* ```typescript | ||
* // TODO | ||
* ``` | ||
*/ | ||
export function map<A, B>(fn: (a: A) => B, child: Decoder<A>) : Decoder<B> | ||
/** | ||
* Similar to {@link Decode.map}, but you return a decoder instead of a | ||
* value. | ||
* This allows you to decide how to continue decoding depending on the | ||
* result. | ||
* | ||
* @param fn - Mapping function that returns a decoder | ||
* @param child - The decoder to run before the mapping function | ||
* | ||
* @typeParam A - Input to the mapping function | ||
* @typeParam B - The mapping function returns a decoder for this type | ||
* | ||
* @example | ||
* Maybe our HTTP API of choice wraps the data in an object that contains | ||
* information about the success of the operation. | ||
* Depending on that value we want to handle the data differently. | ||
* | ||
* ```typescript | ||
* const UserDecoder = Decode.object({ | ||
* id: Decode.field('id', Decode.integer), | ||
* name: Decode.field('name', Decode.string), | ||
* }); | ||
* | ||
* const response_decoder = Decode.andThen( | ||
* success => { | ||
* if (success) { | ||
* return Decode.field('data', UserDecoder); | ||
* } else { | ||
* return Decode.andThen( | ||
* error => Decode.fail('Got an error response: ' + error), | ||
* Decode.field('error', Decode.string), | ||
* ); | ||
* } | ||
* }, | ||
* Decode.field('success', Decode.bool), | ||
* ); | ||
* | ||
* // Works | ||
* decode(response_decoder, { | ||
* success: true, | ||
* data: { | ||
* id: 1, | ||
* name: 'Kobe Bryant', | ||
* }, | ||
* }); | ||
* | ||
* // Fails | ||
* decode(response_decoder, { | ||
* success: false, | ||
* error: 'Could not find user!', | ||
* }); | ||
* ``` | ||
* | ||
* This is nice and all, but it only works for the UserDecoder. | ||
* However, making it generic is rather simple. You just wrap the Decoder | ||
* in a function and accept the data decoder as an argument: | ||
* | ||
* ```typescript | ||
* function responseDecoder<T>(child: Decoder<T>): Decoder<T> { | ||
* return Decode.andThen(success => { | ||
* if (success) { | ||
* return Decode.field('data', child); | ||
* } else { | ||
* // etc. | ||
* } | ||
* }, Decode.field('success', Decode.boolean'); | ||
* } | ||
* ``` | ||
*/ | ||
export function andThen<A, B>(fn: (a: A) => Decoder<B>, child: Decoder<A>) : Decoder<B> | ||
/** | ||
* Combine multiple decoders where any of them can match for the resulting | ||
* decoder to succeed. | ||
* | ||
* @param decoders - A list of decoders that will be executed | ||
* | ||
* @example | ||
* For this example we assume that we have an API endpoint that returns | ||
* either a numeric string or a number. Something like this: | ||
* | ||
* ```json | ||
* [ | ||
* { id: 1 }, | ||
* { id: '2' }, | ||
* ] | ||
* ``` | ||
* | ||
* which can be decoded to a flat numeric array like this: | ||
* | ||
* ```typescript | ||
* const string_to_number_decoder = Decode.andThen(str => { | ||
* const v = parseInt(str, 10); | ||
* | ||
* if (typeof v === 'number' && !isNaN(v)) { | ||
* return Decode.succeed(v); | ||
* } else { | ||
* return Decode.fail(expected('a number string', str)); | ||
* } | ||
* }, Decode.string); | ||
* | ||
* const decoder = Decode.oneOf([ | ||
* Decode.number, | ||
* string_to_number_decoder, | ||
* ]); | ||
* | ||
* // This gives us an array like [1, 2], where both values are actual | ||
* // numbers | ||
* decode(Decode.many(Decode.field('id', decoder)), [ | ||
* { id: 1 }, | ||
* { id: '2' }, | ||
* ]); | ||
* ``` | ||
*/ | ||
export function oneOf<T>(decoders: Decoder<T>[]) : Decoder<T> | ||
/** | ||
* If the decoder fails, the given value is returned instead. | ||
* | ||
* @example | ||
* ```typescript | ||
* const decoder = Decode.optional(42, Decode.number); | ||
* | ||
* decode(decoder, 100) === 100 | ||
* decode(decoder, -10) === -10 | ||
* | ||
* decode(decoder, '3') === 42 | ||
* decode(decoder, NaN) === 42 | ||
* decode(decoder, null) === 42 | ||
* ``` | ||
* | ||
* @remarks This is implemented by using {@link Decode.oneOf}: | ||
* ```typescript | ||
* function optional(value, child) { | ||
* return Decode.oneOf([ child, Decode.succeed(value) ]); | ||
* } | ||
* ``` | ||
*/ | ||
export function optional<T>(value: T, child: Decoder<T>) : Decoder<T> | ||
/** | ||
* Make decoder that runs the given function and uses its result for | ||
* decoding. | ||
* This can be used for nested decoding, where you would otherwise get a | ||
* `Cannot access 'value' before initialization` error. | ||
* | ||
* @param fn - Function that returns the actual decoder | ||
* | ||
* @example | ||
* ```typescript | ||
* class Tree {} | ||
* | ||
* const tree_decoder = Decode.instance(Tree, { | ||
* value: Decode.field('value', Decode.integer), | ||
* children: Decode.field( | ||
* 'children', | ||
* Decode.lazy(() => Decode.many(tree_decoder)), | ||
* ), | ||
* }); | ||
* | ||
* const raw = { | ||
* value: 42, | ||
* children: [ | ||
* { value: 43, children: [] }, | ||
* { | ||
* value: 44, | ||
* children: [ | ||
* { value: 45, children: [] }, | ||
* ], | ||
* }, | ||
* ] | ||
* }; | ||
* | ||
* decode(tree_decoder, raw) // Decodes the nested tree structure | ||
* ``` | ||
* | ||
* @returns A decoder that calls the given function when its executed. The | ||
* returned value is what will then be used for decoding. | ||
*/ | ||
export function lazy<T>(fn: () => Decoder<T>) : Decoder<T> | ||
/** | ||
* Make a decoder that *always* succeeds with the given value. | ||
* The input is basically ignored. | ||
* | ||
* @param value - The returned value | ||
* | ||
* @example | ||
* ```typescript | ||
* const decoder = Decode.succeed(42); | ||
* | ||
* decode(decoder, 'string') === 42 | ||
* decode(decoder, {}) === 42 | ||
* decode(decoder, null) === 42 | ||
* decode(decoder, 42) === 42 | ||
* ``` | ||
* | ||
* @returns A decoder that always succeeds with the given value when executed | ||
*/ | ||
export function succeed<T>(value: T) : Decoder<T> | ||
/** | ||
* Make a decoder that *never* succeeds and fails with the given message. | ||
* This is mostly useful for building custom decoders with `andThen`. | ||
* | ||
* @param message - Custom error message | ||
* | ||
* @example | ||
* This example uses {@link expected} to create the error message. | ||
* | ||
* ```typescript | ||
* const base64_decoder = Decode.andThen(str => { | ||
* try { | ||
* return Decode.succeed(atob(str)); | ||
* } catch { | ||
* return Decode.fail(expected('a valid base64 string', str)); | ||
* } | ||
* }, Decode.string); | ||
* | ||
* decode(base64_decoder, 'SGVsbG8sIFdvcmxkIQ==') === 'Hello, World!' | ||
* decode(base64_decoder, 'invalid$') // Throws | ||
* ``` | ||
* | ||
* @returns A decoder that always fails when executed | ||
*/ | ||
export function fail<T>(message: string) : Decoder<T> | ||
} | ||
/** | ||
* Run the given decoder on the given input. | ||
* | ||
* @param decoder - The decoder to use | ||
* @param json - The unknown value you want to decode | ||
* | ||
* @typeParam T - The result type of the decoder and also return type | ||
* of the function | ||
* | ||
* @throws If any decoder causes an error this function will throw it | ||
* @returns The value as advertised by the decoder | ||
*/ | ||
export function decode<T>(decoder: Decoder<T>, json: any) : T | ||
/** | ||
* This is the same as {@link decode}, but it accepts a JSON string instead of | ||
* a JavaScript value. | ||
* | ||
* @param decoder - The decoder to use | ||
* @param json - The JSON string | ||
* | ||
* @see decode | ||
*/ | ||
export function decodeString<T>(decoder: Decoder<T>, json: string) : T | ||
/** | ||
* Useful for building error messages for your own little decoders. | ||
* Since this function is used internally, the message layout will be the same, | ||
* which makes it easier for humans to parse error messages. Especially when | ||
* decoding complex values. | ||
* | ||
* @param description - Describe what kind of value you expected | ||
* @param value - Whatever value you got instead | ||
* | ||
* @returns A nicely formatted error string | ||
*/ | ||
export function expected(description: string, value: any) : string |
43
index.js
// The export pattern is a UMD template: | ||
// https://github.com/umdjs/umd/blob/1deb860078252f31ced62fa8e7694f8bbfa6d889/templates/returnExports.js | ||
(function (root, factory) { | ||
/* c8 ignore start */ | ||
if (typeof define === 'function' && define.amd) { | ||
@@ -16,2 +17,3 @@ // AMD. Register as an anonymous module. | ||
} | ||
/* c8 ignore stop */ | ||
}(this, function () { | ||
@@ -40,2 +42,4 @@ var Decode = {}; | ||
var DICT = 16; | ||
var FIELD_ERROR_META = 999; | ||
@@ -67,2 +71,3 @@ | ||
/* c8 ignore start */ | ||
function debugReplace(key, value) { | ||
@@ -100,3 +105,3 @@ if (key === '') { | ||
} else { | ||
return '<Object with ' + fieldCount + 'fields, like ' + fieldStr + '>'; | ||
return '<Object with ' + fieldCount + ' fields, like ' + fieldStr + '>'; | ||
} | ||
@@ -107,2 +112,3 @@ } else { | ||
} | ||
/* c8 ignore stop */ | ||
@@ -131,3 +137,3 @@ function toDebugString(value) { | ||
case NUMBER: | ||
if (typeof value !== 'number') { | ||
if (typeof value !== 'number' || isNaN(value)) { | ||
return err(expected('a number', value)); | ||
@@ -139,3 +145,3 @@ } else { | ||
case INTEGER: | ||
if (typeof value !== 'number' || (value | 0) !== value) { | ||
if (typeof value !== 'number' || Math.trunc(value) !== value) { | ||
return err(expected('an integer', value)); | ||
@@ -166,4 +172,2 @@ } else { | ||
} | ||
return decodeInternal(decoder.child, value[decoder.key]); | ||
} | ||
@@ -254,2 +258,23 @@ } | ||
} | ||
case DICT: { | ||
if (typeof value !== 'object' || value === null) { | ||
return err(expected('an object', value)); | ||
} else { | ||
var result = {}; | ||
for (var key in value) { | ||
var child_value = decodeInternal(decoder.child, value[key]); | ||
if (isOk(child_value)) { | ||
result[key] = child_value.value; | ||
} else { | ||
// TODO Wrap in key info | ||
return child_value; | ||
} | ||
} | ||
return ok(result); | ||
} | ||
} | ||
} | ||
@@ -345,2 +370,6 @@ } | ||
Decode.dict = function(child) { | ||
return { tag: DICT, child: child }; | ||
}; | ||
return { | ||
@@ -353,4 +382,6 @@ Decode: Decode, | ||
return decode(decoder, val); | ||
} | ||
}, | ||
expected: expected, | ||
}; | ||
})); |
{ | ||
"name": "@schroffl/json-mapping", | ||
"version": "1.2.1", | ||
"description": "A set of utilites for defining and running conversions between JSON and JavaScript values", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"directories": {}, | ||
"scripts": {}, | ||
"author": "schroffl", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"benchmark": "^2.1.4" | ||
} | ||
"name": "@schroffl/json-mapping", | ||
"version": "2.0.0", | ||
"description": "A set of utilites for defining and running conversions between JSON and JavaScript values", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"directories": {}, | ||
"scripts": { | ||
"test": "c8 -r html ava" | ||
}, | ||
"author": "schroffl", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"ava": "^4.0.1", | ||
"benchmark": "^2.1.4", | ||
"c8": "^7.11.3" | ||
} | ||
} |
# json-mapping | ||
A set of utilites for defining and running conversions between JSON and JavaScript values. | ||
Heavily inspired by [`elm/json`](https://github.com/elm/json). |
339
test.js
@@ -1,15 +0,328 @@ | ||
"use strict"; | ||
exports.__esModule = true; | ||
var index_1 = require("./index"); | ||
var internal = index_1.Decode.object({ | ||
y: index_1.Decode.object({ | ||
z: index_1.Decode.field('x', index_1.Decode.unknown) | ||
}) | ||
const test = require('ava'); | ||
const { Decode, decode, expected, decodeString } = require('./index'); | ||
const make = decoder => decode.bind(undefined, decoder); | ||
test('Decode.string', t => { | ||
const run = make(Decode.string); | ||
t.is(run('string'), 'string'); | ||
t.throws(() => run(42)); | ||
}); | ||
var decoder = index_1.Decode.andThen(function (value) { | ||
return index_1.Decode.succeed(value); | ||
}, internal); | ||
var result = index_1.decode(decoder, { | ||
x: 42 | ||
test('Decode.number', t => { | ||
const run = make(Decode.number); | ||
t.is(run(42), 42); | ||
t.is(run(42.42), 42.42); | ||
t.throws(() => run('42')); | ||
t.throws(() => run(NaN)); | ||
}); | ||
console.log(result); | ||
test('Decode.integer', t => { | ||
const run = make(Decode.integer); | ||
t.is(run(42), 42); | ||
t.is(run(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER); | ||
t.is(run(Number.MIN_SAFE_INTEGER), Number.MIN_SAFE_INTEGER); | ||
t.throws(() => run(42.42)); | ||
t.throws(() => run('42')); | ||
t.throws(() => run(NaN)); | ||
}); | ||
test('Decode.bool', t => { | ||
const run = make(Decode.bool); | ||
t.is(run(false), false); | ||
t.is(run(true), true); | ||
t.throws(() => run(0)); | ||
t.throws(() => run(1)); | ||
t.throws(() => run('true')); | ||
t.throws(() => run('false')); | ||
t.throws(() => run(null)); | ||
t.throws(() => run(undefined)); | ||
t.throws(() => run('')); | ||
}); | ||
test('Decode.unknown', t => { | ||
const run = make(Decode.unknown); | ||
const ref = {}; | ||
const sym = Symbol(); | ||
t.is(run('abc'), 'abc'); | ||
t.is(run(false), false); | ||
t.is(run(Number.MAX_VALUE), Number.MAX_VALUE); | ||
t.is(run(ref), ref); | ||
t.is(run(sym), sym); | ||
}); | ||
test('Decode.succeed', t => { | ||
const run = make(Decode.succeed(42)); | ||
t.is(run(undefined), 42); | ||
t.is(run(null), 42); | ||
t.is(run('ababababa'), 42); | ||
t.is(run({}), 42); | ||
t.is(run(Symbol()), 42); | ||
t.is(run(42.2), 42); | ||
t.is(run(42), 42); | ||
}); | ||
test('Decode.fail', t => { | ||
const run = make(Decode.fail('fail')); | ||
t.throws(() => run(undefined)); | ||
t.throws(() => run(null)); | ||
t.throws(() => run('ababababa')); | ||
t.throws(() => run({})); | ||
t.throws(() => run(Symbol())); | ||
t.throws(() => run(42.2)); | ||
t.throws(() => run(42)); | ||
}); | ||
test('Decode.field', t => { | ||
const run = make(Decode.field('value', Decode.integer)); | ||
t.is(run({ value: 42 }), 42); | ||
t.is(run({ value: -42 }), -42); | ||
t.throws(() => run(null)); | ||
t.throws(() => run({ })); | ||
t.throws(() => run({ value: '42' })); | ||
t.throws(() => run({ valu: 42 })); | ||
t.throws(() => run({ value: { value: 42 } })); | ||
}); | ||
test('Decode.at', t => { | ||
const run = make(Decode.at(['a', 'b', 'c', 'd'], Decode.integer)); | ||
t.is(run({a: {b: {c: {d: 42 }}}}), 42); | ||
t.throws(() => run({a: {b: {c: {e: 42 }}}})); | ||
t.throws(() => run({a: {b: {c: {d: '42' }}}})); | ||
t.throws(() => run({a: {d: '42' }})); | ||
}); | ||
test('Decode.many', t => { | ||
const run = make(Decode.many(Decode.integer)); | ||
t.deepEqual(run([]), []); | ||
t.deepEqual(run([1, 2, 3]), [1, 2, 3]); | ||
t.throws(() => run(['1', 2, 3])); | ||
t.throws(() => run({})); | ||
t.throws(() => run(null)); | ||
t.throws(() => run(undefined)); | ||
}); | ||
test('Decode.oneOf', t => { | ||
const string_to_int_decoder = Decode.andThen(str => { | ||
const v = parseInt(str, 10); | ||
if (typeof v === 'number' && !isNaN(v) && Math.trunc(v) === v) { | ||
return Decode.succeed(v); | ||
} else { | ||
return Decode.fail(expected('a number string', str)); | ||
} | ||
}, Decode.string); | ||
const decoder = Decode.oneOf([ | ||
Decode.integer, | ||
string_to_int_decoder, | ||
]); | ||
const result = decode(Decode.many(Decode.field('id', decoder)), [ | ||
{ id: 1 }, | ||
{ id: '2' }, | ||
]); | ||
t.deepEqual(result, [1, 2]); | ||
const failing = Decode.oneOf([ | ||
Decode.integer, | ||
Decode.string, | ||
]); | ||
t.throws(() => decode(failing, true)); | ||
}); | ||
test('Decode.andThen', t => { | ||
const run = make( | ||
Decode.andThen( | ||
v => v > 100 ? Decode.succeed('big') : Dec.fail('too small'), | ||
Decode.integer, | ||
), | ||
); | ||
t.is(run(101), 'big'); | ||
t.throws(() => run(42)); | ||
t.throws(() => run(100)); | ||
t.throws(() => run('42')); | ||
}); | ||
test('Decode.dict', t => { | ||
const run = make(Decode.dict(Decode.integer)); | ||
const arg = { a: 42, b: 102 }; | ||
const result = run({ a: 42, b: 102 }); | ||
t.not(result, arg); // They should *not* be the same object | ||
t.deepEqual(result, arg); | ||
t.throws(() => run({ a: 42, b: 102, c: '3' })); | ||
t.throws(() => run({ a: 42, b: 102, c: 42.2 })); | ||
t.throws(() => run('abc')); | ||
t.throws(() => run(null)); | ||
}); | ||
test('Decode.object', t => { | ||
const run = make(Decode.object({ value: Decode.integer })); | ||
t.deepEqual(run(42), { value: 42 }); | ||
// Nested object | ||
const run_other = make( | ||
Decode.object({ | ||
name: Decode.field('name', Decode.string), | ||
position: Decode.object({ | ||
latitude: Decode.field('lat', Decode.number), | ||
longitude: Decode.field('lng', Decode.number), | ||
}), | ||
}), | ||
); | ||
t.deepEqual( | ||
run_other({ | ||
name: 'Taj Mahal', | ||
lat: 27.175000, | ||
lng: 78.041944, | ||
}), | ||
{ | ||
name: 'Taj Mahal', | ||
position: { | ||
latitude: 27.175000, | ||
longitude: 78.041944, | ||
}, | ||
}, | ||
); | ||
t.throws(() => { | ||
run_other({ | ||
name: 'abc', | ||
lat: '27', | ||
lng: '78', | ||
}); | ||
}); | ||
}); | ||
test('Decode.instance', t => { | ||
class User { | ||
initials() { | ||
const parts = this.name.split(' '); | ||
return parts[0][0] + parts[1][0]; | ||
} | ||
} | ||
const UserDecoder = Decode.instance(User, { | ||
id: Decode.field('id', Decode.integer), | ||
name: Decode.field('name', Decode.string), | ||
}); | ||
const user = decode(UserDecoder, { id: 42, name: 'Yo Bo' }); | ||
t.true(user instanceof User); | ||
t.is(user.initials(), 'YB'); | ||
t.throws(() => decode(UserDecoder, {})); | ||
}); | ||
test('Decode.lazy', t => { | ||
const Tree = class {}; | ||
const Tree_decoder = Decode.instance(Tree, { | ||
value: Decode.field('value', Decode.integer), | ||
nodes: Decode.field( | ||
'nodes', | ||
Decode.lazy(() => Decode.many(Tree_decoder)), | ||
), | ||
}); | ||
const raw = { | ||
value: 42, | ||
nodes: [ | ||
{ value: 103, nodes: [] }, | ||
{ | ||
value: 104, | ||
nodes: [], | ||
}, | ||
] | ||
}; | ||
const tree = decode(Tree_decoder, raw); | ||
t.is(tree.value, 42); | ||
t.is(tree.nodes[0].value, 103); | ||
t.is(tree.nodes[1].value, 104); | ||
t.true(tree instanceof Tree); | ||
t.true(tree.nodes[0] instanceof Tree); | ||
t.true(tree.nodes[1] instanceof Tree); | ||
}); | ||
test('Decode.optional', t => { | ||
const run = make(Decode.optional(42, Decode.integer)); | ||
t.is(run(100), 100); | ||
t.is(run(-10), -10); | ||
t.is(run(NaN), 42); | ||
t.is(run('3'), 42); | ||
t.is(run(null), 42); | ||
}); | ||
test('Decode.map', t => { | ||
const run = make(Decode.map(n => n * 10, Decode.integer)); | ||
t.is(run(1), 10); | ||
t.is(run(10), 100); | ||
t.is(run(0), 0); | ||
t.throws(() => run('10')); | ||
t.throws(() => run('0')); | ||
}); | ||
test('expected', t => { | ||
const msg = expected('some type'); | ||
t.true(typeof msg === 'string'); | ||
}); | ||
// `decodeString` should produce the same result as `decode`. | ||
test('decodeString', t => { | ||
const cases = [ | ||
{ value: true, decoder: Decode.bool }, | ||
{ value: 100, decoder: Decode.integer }, | ||
{ value: 100.1, decoder: Decode.integer }, | ||
{ value: 100.1, decoder: Decode.number }, | ||
{ value: { a: 42 }, decoder: Decode.field('a', Decode.integer) }, | ||
]; | ||
cases.map(caze => { | ||
const str = JSON.stringify(caze.value); | ||
return { | ||
normal: () => decode(caze.decoder, caze.value), | ||
stringified: () => decodeString(caze.decoder, str), | ||
}; | ||
}).forEach(caze => { | ||
let result; | ||
try { | ||
result = caze.normal(); | ||
} catch (e) { | ||
t.throws(caze.stringified); | ||
return; | ||
} | ||
t.deepEqual(caze.stringified(), result); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
44406
234.33%8
14.29%1260
277.25%3
200%2
100%1
Infinity%