From 5c53c44e49b59c88ce6ddaa26488e63793f234d2 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Mon, 13 Aug 2018 00:19:53 -0400 Subject: [PATCH 1/8] Remove type annotations from combinators This commit will get dropped once the general cleanups PR is merged with this change. --- src/combinators.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/combinators.ts b/src/combinators.ts index 4442e52..99ccc19 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -1,4 +1,4 @@ -import {Decoder, DecoderObject} from './decoder'; +import {Decoder} from './decoder'; /** See `Decoder.string` */ export function string(): Decoder { @@ -27,9 +27,7 @@ export function constant(value: any): Decoder { } /** See `Decoder.object` */ -export function object(decoders: DecoderObject): Decoder { - return Decoder.object(decoders); -} +export const object = Decoder.object; /** See `Decoder.array` */ export const array: (decoder: Decoder) => Decoder = Decoder.array; @@ -38,7 +36,7 @@ export const array: (decoder: Decoder) => Decoder = Decoder.array; export const dict: (decoder: Decoder) => Decoder<{[name: string]: A}> = Decoder.dict; /** See `Decoder.optional` */ -export const optional: (decoder: Decoder) => Decoder = Decoder.optional; +export const optional = Decoder.optional; /** See `Decoder.oneOf` */ export const oneOf: (...decoders: Decoder[]) => Decoder = Decoder.oneOf; From c34f12aa790e0080c4f63a5c8d6b421e8511e3c7 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Mon, 13 Aug 2018 00:23:47 -0400 Subject: [PATCH 2/8] implement OptionalDecoder class to replace optional --- src/combinators.ts | 4 +- src/decoder.ts | 173 +++++++++++++++++++++++++++++---------- test/json-decode.test.ts | 14 +++- 3 files changed, 144 insertions(+), 47 deletions(-) diff --git a/src/combinators.ts b/src/combinators.ts index 99ccc19..59e92bc 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -1,4 +1,4 @@ -import {Decoder} from './decoder'; +import {Decoder, OptionalDecoder} from './decoder'; /** See `Decoder.string` */ export function string(): Decoder { @@ -36,7 +36,7 @@ export const array: (decoder: Decoder) => Decoder = Decoder.array; export const dict: (decoder: Decoder) => Decoder<{[name: string]: A}> = Decoder.dict; /** See `Decoder.optional` */ -export const optional = Decoder.optional; +export const optional = OptionalDecoder.optional; /** See `Decoder.oneOf` */ export const oneOf: (...decoders: Decoder[]) => Decoder = Decoder.oneOf; diff --git a/src/decoder.ts b/src/decoder.ts index 9b2bfb7..b8de966 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -14,25 +14,44 @@ export interface DecoderError { } /** - * Defines a mapped type over an interface `A`. `DecoderObject` is an - * interface that has all the keys or `A`, but each key's property type is - * mapped to a decoder for that type. This type is used when creating decoders - * for objects. + * Helper type with no semantic meaning, used as part of a trick in + * `DecoderObject` to distinguish between optional properties and properties + * that may have a value of undefined, but aren't optional. + */ +type HideUndefined = {}; + +/** + * Defines a mapped type over an interface `A`. This type is used when creating + * decoders for objects. + * + * `DecoderObject` is an interface that has all the properties or `A`, but + * each property's type is mapped to a decoder for that type. If a property is + * required in `A`, the decoder type is `Decoder`. If a property is + * optional in `A`, then that property is required in `DecoderObject`, but + * the decoder type is `OptionalDecoder | Decoder`. + * + * The `OptionalDecoder` type is only returned by the `optional` decoder. * * Example: * ``` - * interface X { + * interface ABC { * a: boolean; - * b: string; + * b?: string; + * c: number | undefined; * } * - * const decoderObject: DecoderObject = { - * a: boolean(), - * b: string() + * DecoderObject === { + * a: Decoder; + * b: OptionalDecoder | Decoder; + * c: Decoder; * } * ``` */ -export type DecoderObject = {[t in keyof A]: Decoder}; +export type DecoderObject = { + [P in keyof T]-?: undefined extends {[Q in keyof T]: HideUndefined}[P] + ? OptionalDecoder> | Decoder> + : Decoder +}; /** * Type guard for `DecoderError`. One use case of the type guard is in the @@ -112,6 +131,8 @@ const prependAt = (newAt: string, {at, ...rest}: Partial): Partial * things with a `Result` as with the decoder methods. */ export class Decoder { + readonly _kind = 'Decoder'; + /** * The Decoder class constructor is kept private to separate the internal * `decode` function from the external `run` function. The distinction @@ -215,12 +236,13 @@ export class Decoder { * isBig: boolean; * } * - * const bearDecoder1: Decoder = object({ + * const bearDecoder1 = object({ * kind: constant('bear'), * isBig: boolean() * }); - * // Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to - * // type 'Decoder'. Type 'string' is not assignable to type '"bear"'. + * // Types of property 'kind' are incompatible. + * // Type 'Decoder' is not assignable to type 'Decoder<"bear">'. + * // Type 'string' is not assignable to type '"bear"'. * * const bearDecoder2: Decoder = object({ * kind: constant<'bear'>('bear'), @@ -280,15 +302,17 @@ export class Decoder { let obj: any = {}; for (const key in decoders) { if (decoders.hasOwnProperty(key)) { - const 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) { + // hack: type as any to access the private `decode` method on OptionalDecoder + const decoder: any = decoders[key]; + const r = decoder.decode(json[key]); + if ( + (r.ok === true && decoder._kind === 'Decoder') || + (r.ok === true && decoder._kind === 'OptionalDecoder' && r.result !== undefined) + ) { + obj[key] = r.result; + } else if (r.ok === false && json[key] === undefined) { return Result.err({message: `the key '${key}' is required but was not present`}); - } else { + } else if (r.ok === false) { return Result.err(prependAt(`.${key}`, r.error)); } } @@ -363,28 +387,6 @@ export class Decoder { } }); - /** - * 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 = object({ - * id: number(), - * isOwner: optional(boolean()) - * }); - * ``` - */ - static optional = (decoder: Decoder): Decoder => - new Decoder( - (json: any) => (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. @@ -655,3 +657,88 @@ export class Decoder { Result.andThen(value => f(value).decode(json), this.decode(json)) ); } + +/** + * The `optional` decoder is given it's own type, the `OptionalDecoder` type, + * since it behaves differently from the other decoders. This decoder has no + * `run` method, so it can't be directly used to test a value. Instead, the + * `object` decoder accepts `optional` for decoding object properties that have + * been marked as optional with the `field?: value` notation. + */ +export class OptionalDecoder { + readonly _kind = 'OptionalDecoder'; + + private constructor( + private decode: (json: any) => Result.Result> + ) {} + + /** + * Decoder to designate that a property may not be present in an object. The + * behavior of `optional` is distinct from using `constant(undefined)` in + * that when the property is not found in the input, the key will not be + * present in the decoded value. + * + * Example: + * ``` + * // type with explicit undefined property + * interface Breakfast1 { + * eggs: number; + * withBacon: boolean | undefined; + * } + * + * // type with optional property + * interface Breakfast2 { + * eggs: number; + * withBacon?: boolean; + * } + * + * // in the first case we can't use `optional` + * breakfast1Decoder = object({ + * eggs: number(), + * withBacon: union(boolean(), constant(undefined)) + * }); + * + * // in the second case we can + * breakfast2Decoder = object({ + * eggs: number(), + * withBacon: optional(boolean()) + * }); + * + * breakfast1Decoder.run({eggs: 12}) + * // => {ok: true, result: {eggs: 12, withBacon: undefined}} + * + * breakfast2Decoder.run({eggs: 7}) + * // => {ok: true, result: {eggs: 7}} + * ``` + */ + static optional = (decoder: Decoder): OptionalDecoder => + new OptionalDecoder( + // hack: type decoder as any to access the private `decode` method on Decoder + (json: any) => (json === undefined ? Result.ok(undefined) : (decoder as any).decode(json)) + ); + + /** + * See `Decoder.prototype.map`. The function `f` is only executed if the + * optional decoder successfuly finds and decodes a value. + */ + map = (f: (value: A) => B): OptionalDecoder => + new OptionalDecoder((json: any) => + Result.map( + (value: A | undefined) => (value === undefined ? undefined : f(value)), + this.decode(json) + ) + ); + + /** + * See `Decoder.prototype.andThen`. The function `f` is only executed if the + * optional decoder successfuly finds and decodes a value. + */ + andThen = (f: (value: A) => Decoder): OptionalDecoder => + new OptionalDecoder((json: any) => + Result.andThen( + (value: A | undefined) => + value === undefined ? Result.ok(undefined) : (f(value) as any).decode(json), + this.decode(json) + ) + ); +} diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts index 8cdfe88..cbf038b 100644 --- a/test/json-decode.test.ts +++ b/test/json-decode.test.ts @@ -179,8 +179,18 @@ describe('object', () => { }); it('can decode a nested object', () => { - const decoder = object({ - payload: object({x: number(), y: number()}), + interface Point { + x: number; + y: number; + } + + interface Location { + payload: Point; + error: false; + } + + const decoder = object({ + payload: object({x: number(), y: number()}), error: constant(false) }); const json = {payload: {x: 5, y: 2}, error: false}; From c4bdce71f01b6d1f787bea50cf228a1b3e561ae0 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Mon, 13 Aug 2018 00:24:08 -0400 Subject: [PATCH 3/8] Update tests --- test/json-decode.test.ts | 86 +++++++++++++++++++++++--------------- test/phone-example.test.ts | 9 ++-- test/user-example.test.ts | 4 +- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts index cbf038b..c32e23f 100644 --- a/test/json-decode.test.ts +++ b/test/json-decode.test.ts @@ -124,7 +124,7 @@ describe('constant', () => { interface TrueValue { x: true; } - const decoder: Decoder = object({x: constant(true)}); + const decoder: Decoder = object({x: constant(true)}); expect(decoder.run({x: true})).toEqual({ok: true, result: {x: true}}); }); @@ -133,7 +133,7 @@ describe('constant', () => { interface FalseValue { x: false; } - const decoder: Decoder = object({x: constant(false)}); + const decoder = object({x: constant(false)}); expect(decoder.run({x: false})).toEqual({ok: true, result: {x: false}}); }); @@ -142,7 +142,7 @@ describe('constant', () => { interface NullValue { x: null; } - const decoder: Decoder = object({x: constant(null)}); + const decoder = object({x: constant(null)}); expect(decoder.run({x: null})).toEqual({ok: true, result: {x: null}}); }); @@ -253,14 +253,53 @@ describe('object', () => { }); }); - it('ignores optional fields that decode to undefined', () => { - const decoder = object({ - a: number(), - b: optional(string()) + describe('optional and undefined fields', () => { + it('ignores optional fields that decode to undefined', () => { + interface AB { + a: number; + b?: string; + } + + const decoder: Decoder = object({ + a: number(), + b: optional(string()) + }); + + expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}}); + expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12}}); }); - expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}}); - expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12}}); + it('includes fields that are mapped to a value when not found', () => { + interface AB { + a: number; + b: string; + } + + const decoder: Decoder = object({ + a: number(), + b: oneOf(string(), constant(undefined)).map( + (b: string | undefined) => (b === undefined ? 'b not found' : b) + ) + }); + + expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}}); + expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12, b: 'b not found'}}); + }); + + it('includes fields that are mapped to a undefined when not found', () => { + interface AB { + a: number; + b: string | undefined; + } + + const decoder: Decoder = object({ + a: number(), + b: oneOf(string(), constant(undefined)) + }); + + expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}}); + expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12, b: undefined}}); + }); }); }); @@ -344,32 +383,13 @@ describe('dict', () => { }); describe('optional', () => { - describe('decoding a non-object type', () => { - const decoder = optional(number()); - - it('can decode the given type', () => { - expect(decoder.run(5)).toEqual({ok: true, result: 5}); - }); - - it('can decode undefined', () => { - expect(decoder.run(undefined)).toEqual({ok: true, result: undefined}); - }); - - it('fails when the value is invalid', () => { - expect(decoder.run(false)).toMatchObject({ - ok: false, - error: {at: 'input', message: 'expected a number, got a boolean'} - }); - }); - }); - describe('decoding an interface with optional fields', () => { interface User { id: number; isDog?: boolean; } - const decoder: Decoder = object({ + const decoder = object({ id: number(), isDog: optional(boolean()) }); @@ -459,8 +479,8 @@ describe('union', () => { type C = A | B; const decoder: Decoder = union( - object({kind: constant<'a'>('a'), value: number()}), - object({kind: constant<'b'>('b'), value: boolean()}) + object({kind: constant<'a'>('a'), value: number()}), + object({kind: constant<'b'>('b'), value: boolean()}) ); it('can decode a value that matches one of the union types', () => { @@ -537,7 +557,7 @@ describe('valueAt', () => { }); describe('decode an optional field', () => { - const decoder = valueAt(['a', 'b', 'c'], optional(string())); + const decoder = valueAt(['a', 'b', 'c'], oneOf(string(), constant(undefined))); it('fails when the path does not exist', () => { const error = decoder.run({a: {x: 'cats'}}); @@ -638,7 +658,7 @@ describe('lazy', () => { replies: Comment[]; } - const decoder: Decoder = object({ + const decoder: Decoder = object({ msg: string(), replies: lazy(() => array(decoder)) }); diff --git a/test/phone-example.test.ts b/test/phone-example.test.ts index a179ca9..be554b8 100644 --- a/test/phone-example.test.ts +++ b/test/phone-example.test.ts @@ -42,14 +42,14 @@ describe('decode phone number objects', () => { constant(PhoneUse.Work) ); - const internationalPhoneDecoder: Decoder = object({ + const internationalPhoneDecoder = object({ id: number(), use: optional(phoneUseDecoder), international: constant(true), rawNumber: string() }); - const domesticPhoneDecoder: Decoder = object({ + const domesticPhoneDecoder = object({ id: number(), use: optional(phoneUseDecoder), international: constant(false), @@ -58,7 +58,10 @@ describe('decode phone number objects', () => { lineNumber: string() }); - const phoneDecoder: Decoder = union(domesticPhoneDecoder, internationalPhoneDecoder); + const phoneDecoder: Decoder = union( + domesticPhoneDecoder, + internationalPhoneDecoder + ); const phonesDecoder: Decoder = array(phoneDecoder); diff --git a/test/user-example.test.ts b/test/user-example.test.ts index 9a620dc..fc7ab36 100644 --- a/test/user-example.test.ts +++ b/test/user-example.test.ts @@ -1,4 +1,4 @@ -import {Decoder, string, number, boolean, object} from '../src/index'; +import {string, number, boolean, object} from '../src/index'; describe('decode json as User interface', () => { interface User { @@ -22,7 +22,7 @@ describe('decode json as User interface', () => { active: false }; - const userDecoder: Decoder = object({ + const userDecoder = object({ firstname: string(), lastname: string(), age: number(), From 80de1f0b288288336ee2a09c5d91553dbcd937b4 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Fri, 17 Aug 2018 16:05:11 -0400 Subject: [PATCH 4/8] Update constant docs and types --- src/decoder.ts | 13 +++++++------ test/json-decode.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/decoder.ts b/src/decoder.ts index b8de966..18640cb 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -218,6 +218,7 @@ export class Decoder { * | constant(true) | Decoder | * | constant(false) | Decoder | * | constant(null) | Decoder | + * | constant(undefined) | Decoder | * | constant('alaska') | Decoder | * | constant<'alaska'>('alaska') | Decoder<'alaska'> | * | constant(50) | Decoder | @@ -474,18 +475,18 @@ export class Decoder { * ``` * * 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. + * path, even if the last key is not found. This allows the value to be + * `undefined` when appropriate. * ``` - * const optionalDecoder = valueAt(['a', 'b', 'c'], optional(string())); + * const decoder = valueAt(['a', 'b', 'c'], union(string(), constant(undefined))); * - * optionalDecoder.run({a: {b: {c: 'surprise!'}}}) + * decoder.run({a: {b: {c: 'surprise!'}}}) * // => {ok: true, result: 'surprise!'} * - * optionalDecoder.run({a: {b: 'cats'}}) + * decoder.run({a: {b: 'cats'}}) * // => {ok: false, error: {... at: 'input.a.b.c' message: 'expected an object, got "cats"'} * - * optionalDecoder.run({a: {b: {z: 1}}}) + * decoder.run({a: {b: {z: 1}}}) * // => {ok: true, result: undefined} * ``` */ diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts index c32e23f..b78a808 100644 --- a/test/json-decode.test.ts +++ b/test/json-decode.test.ts @@ -147,6 +147,22 @@ describe('constant', () => { expect(decoder.run({x: null})).toEqual({ok: true, result: {x: null}}); }); + it('can decode undefined', () => { + interface UndefinedValue { + a: string; + b: undefined; + } + const decoder = object({a: string(), b: constant(undefined)}); + + const run1 = decoder.run({a: 'qwerty', b: undefined}); + expect(run1).toEqual({ok: true, result: {a: 'qwerty', b: undefined}}); + expect(Result.map(Object.keys, run1)).toEqual({ok: true, result: ['a', 'b']}); + + const run2 = decoder.run({a: 'asdfgh'}); + expect(run2).toEqual({ok: true, result: {a: 'asdfgh', b: undefined}}); + expect(Result.map(Object.keys, run2)).toEqual({ok: true, result: ['a', 'b']}); + }); + it('can decode a constant array', () => { type A = [1, 2, 3]; const decoder: Decoder = constant([1, 2, 3]); From faaa15e822770de1b80ba5d847542e47a3672724 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Fri, 17 Aug 2018 16:05:50 -0400 Subject: [PATCH 5/8] update tests for optional --- test/json-decode.test.ts | 47 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts index b78a808..a5f644b 100644 --- a/test/json-decode.test.ts +++ b/test/json-decode.test.ts @@ -399,12 +399,12 @@ describe('dict', () => { }); describe('optional', () => { - describe('decoding an interface with optional fields', () => { - interface User { - id: number; - isDog?: boolean; - } + interface User { + id: number; + isDog?: boolean; + } + describe('decoding an interface with optional fields', () => { const decoder = object({ id: number(), isDog: optional(boolean()) @@ -426,6 +426,36 @@ describe('optional', () => { }); }); }); + + it('supports map', () => { + const decoder = object({ + id: number(), + isDog: optional(number()).map(num => num !== 0) + }); + + expect(decoder.run({id: 1, isDog: 0})).toEqual({ok: true, result: {id: 1, isDog: false}}); + expect(decoder.run({id: 1, isDog: 77})).toEqual({ok: true, result: {id: 1, isDog: true}}); + expect(decoder.run({id: 1})).toEqual({ok: true, result: {id: 1}}); + }); + + it('supports andThen', () => { + const decoder = object({ + id: number(), + isDog: optional(string()).andThen( + dogName => + dogName.toLowerCase()[0] === 'd' + ? succeed(true) + : fail(`${dogName} is not a dog, all dog names start with 'D'`) + ) + }); + + expect(decoder.run({id: 1, isDog: 'Doug'})).toEqual({ok: true, result: {id: 1, isDog: true}}); + expect(decoder.run({id: 1, isDog: 'Wanda'})).toMatchObject({ + ok: false, + error: {message: "Wanda is not a dog, all dog names start with 'D'"} + }); + expect(decoder.run({id: 1})).toEqual({ok: true, result: {id: 1}}); + }); }); describe('oneOf', () => { @@ -531,7 +561,7 @@ describe('withDefault', () => { }); describe('valueAt', () => { - describe('decode an value', () => { + describe('decode a value accessed from a path', () => { it('can decode a single object field', () => { const decoder = valueAt(['a'], string()); expect(decoder.run({a: 'boots', b: 'cats'})).toEqual({ok: true, result: 'boots'}); @@ -573,7 +603,10 @@ describe('valueAt', () => { }); describe('decode an optional field', () => { - const decoder = valueAt(['a', 'b', 'c'], oneOf(string(), constant(undefined))); + const decoder: Decoder = valueAt( + ['a', 'b', 'c'], + union(string(), constant(undefined)) + ); it('fails when the path does not exist', () => { const error = decoder.run({a: {x: 'cats'}}); From e306ccecb02b438dbab0a18f19758b8cbd7e58c6 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Sun, 19 Aug 2018 21:59:10 -0400 Subject: [PATCH 6/8] Add @hidden flag to class properties that don't need to be in the genrated docs --- src/decoder.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/decoder.ts b/src/decoder.ts index 18640cb..e57817a 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -131,6 +131,9 @@ const prependAt = (newAt: string, {at, ...rest}: Partial): Partial * things with a `Result` as with the decoder methods. */ export class Decoder { + /** + * @hidden + */ readonly _kind = 'Decoder'; /** @@ -146,6 +149,8 @@ export class Decoder { * provided decoder combinators and helper functions such as * `andThen` and `map` should be enough to build specialized decoders as * needed. + * + * @hidden */ private constructor(private decode: (json: any) => Result.Result>) {} @@ -667,8 +672,14 @@ export class Decoder { * been marked as optional with the `field?: value` notation. */ export class OptionalDecoder { + /** + * @hidden + */ readonly _kind = 'OptionalDecoder'; + /** + * @hidden + */ private constructor( private decode: (json: any) => Result.Result> ) {} From ce7e06aa1947a4f837e48b80ab366bcda5436688 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Sun, 19 Aug 2018 21:54:48 -0400 Subject: [PATCH 7/8] upgrade typedoc --- package.json | 2 +- yarn.lock | 86 ++++++++++++++++++++++++++-------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 7c4a72b..9f15e85 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "tslint": "^5.8.0", "tslint-config-prettier": "^1.1.0", "tslint-config-standard": "^7.0.0", - "typedoc": "^0.11.1", + "typedoc": "^0.12.0", "typedoc-plugin-markdown": "^1.0.13", "typescript": "~2.9.2" } diff --git a/yarn.lock b/yarn.lock index ea2fb28..853307a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,9 +26,9 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" -"@types/fs-extra@5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.1.tgz#cd856fbbdd6af2c11f26f8928fd8644c9e9616c9" +"@types/fs-extra@^5.0.3": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.4.tgz#b971134d162cc0497d221adde3dbb67502225599" dependencies: "@types/node" "*" @@ -40,29 +40,29 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/handlebars@4.0.36": - version "4.0.36" - resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.36.tgz#ff57c77fa1ab6713bb446534ddc4d979707a3a79" +"@types/handlebars@^4.0.38": + version "4.0.39" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd" -"@types/highlight.js@9.12.2": - version "9.12.2" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.2.tgz#6ee7cd395effe5ec80b515d3ff1699068cd0cd1d" +"@types/highlight.js@^9.12.3": + version "9.12.3" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" "@types/jest@^22.2.3": version "22.2.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d" -"@types/lodash@4.14.104": - version "4.14.104" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" - "@types/lodash@^4.14.0": version "4.14.114" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.114.tgz#e6f251af5994dd0d7ce141f9241439b4f40270f6" -"@types/marked@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524" +"@types/lodash@^4.14.110": + version "4.14.116" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9" + +"@types/marked@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.4.0.tgz#057a6165703e7419217f8ffc6887747f980b6315" "@types/minimatch@*", "@types/minimatch@3.0.3": version "3.0.3" @@ -72,9 +72,9 @@ version "10.5.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.3.tgz#5bcfaf088ad17894232012877669634c06b20cc5" -"@types/shelljs@0.7.8": - version "0.7.8" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.8.tgz#4b4d6ee7926e58d7bca448a50ba442fd9f6715bd" +"@types/shelljs@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.0.tgz#0caa56b68baae4f68f44e0dd666ab30b098e3632" dependencies: "@types/glob" "*" "@types/node" "*" @@ -1157,9 +1157,9 @@ fs-extra@^4.0.2: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" +fs-extra@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6" dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" @@ -2173,9 +2173,9 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.3.17: - version "0.3.19" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" +marked@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" math-random@^1.0.1: version "1.0.1" @@ -2986,7 +2986,7 @@ shell-quote@^1.6.1: array-reduce "~0.0.0" jsonify "~0.0.0" -shelljs@^0.8.1: +shelljs@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" dependencies: @@ -3420,31 +3420,31 @@ typedoc-plugin-markdown@^1.0.13: dependencies: "@forked/turndown" "^4.0.4" -typedoc@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.11.1.tgz#9f033887fd2218c769e1045feb88a1efed9f12c9" +typedoc@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.12.0.tgz#c5d606f52af29d841658e18d9faa1a72acf0e270" dependencies: - "@types/fs-extra" "5.0.1" - "@types/handlebars" "4.0.36" - "@types/highlight.js" "9.12.2" - "@types/lodash" "4.14.104" - "@types/marked" "0.3.0" + "@types/fs-extra" "^5.0.3" + "@types/handlebars" "^4.0.38" + "@types/highlight.js" "^9.12.3" + "@types/lodash" "^4.14.110" + "@types/marked" "^0.4.0" "@types/minimatch" "3.0.3" - "@types/shelljs" "0.7.8" - fs-extra "^5.0.0" + "@types/shelljs" "^0.8.0" + fs-extra "^7.0.0" handlebars "^4.0.6" highlight.js "^9.0.0" - lodash "^4.17.5" - marked "^0.3.17" + lodash "^4.17.10" + marked "^0.4.0" minimatch "^3.0.0" progress "^2.0.0" - shelljs "^0.8.1" + shelljs "^0.8.2" typedoc-default-themes "^0.5.0" - typescript "2.7.2" + typescript "3.0.x" -typescript@2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" +typescript@3.0.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb" typescript@~2.9.2: version "2.9.2" From caf597fdcb3c72e1ef692c1e6f9a9f5e5401d021 Mon Sep 17 00:00:00 2001 From: Elias Mulhall Date: Sun, 19 Aug 2018 21:55:07 -0400 Subject: [PATCH 8/8] regenerate docs --- docs/classes/_decoder_.decoder.md | 79 ++---------- docs/classes/_decoder_.optionaldecoder.md | 144 ++++++++++++++++++++++ docs/modules/_combinators_.md | 47 ++----- docs/modules/_decoder_.md | 19 ++- 4 files changed, 178 insertions(+), 111 deletions(-) create mode 100644 docs/classes/_decoder_.optionaldecoder.md diff --git a/docs/classes/_decoder_.decoder.md b/docs/classes/_decoder_.decoder.md index d1bf935..f1a715c 100644 --- a/docs/classes/_decoder_.decoder.md +++ b/docs/classes/_decoder_.decoder.md @@ -18,10 +18,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result ## Index -### Constructors - -* [constructor](_decoder_.decoder.md#constructor) - ### Properties * [decode](_decoder_.decoder.md#decode) @@ -43,7 +39,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result * [number](_decoder_.decoder.md#number) * [object](_decoder_.decoder.md#object) * [oneOf](_decoder_.decoder.md#oneof) -* [optional](_decoder_.decoder.md#optional) * [string](_decoder_.decoder.md#string) * [succeed](_decoder_.decoder.md#succeed) * [union](_decoder_.decoder.md#union) @@ -52,28 +47,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result --- -## Constructors - - - -### `` constructor - -⊕ **new Decoder**(decode: *`function`*): [Decoder](_decoder_.decoder.md) - -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` 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` is turned into a `DecoderError` by filling in the missing informaiton. - -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. - -**Parameters:** - -| Param | Type | -| ------ | ------ | -| decode | `function` | - -**Returns:** [Decoder](_decoder_.decoder.md) - -___ - ## Properties @@ -331,6 +304,7 @@ Providing the type parameter is only necessary for type-literal strings and numb | constant(true) | Decoder | | constant(false) | Decoder | | constant(null) | Decoder | + | constant(undefined) | Decoder | | constant('alaska') | Decoder | | constant<'alaska'>('alaska') | Decoder<'alaska'> | | constant(50) | Decoder | @@ -349,12 +323,13 @@ interface Bear { isBig: boolean; } -const bearDecoder1: Decoder = object({ +const bearDecoder1 = object({ kind: constant('bear'), isBig: boolean() }); -// Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to -// type 'Decoder'. Type 'string' is not assignable to type '"bear"'. +// Types of property 'kind' are incompatible. +// Type 'Decoder' is not assignable to type 'Decoder<"bear">'. +// Type 'string' is not assignable to type '"bear"'. const bearDecoder2: Decoder = object({ kind: constant<'bear'>('bear'), @@ -561,40 +536,6 @@ oneOf(constant('start'), constant('stop'), succeed('unknown')) **Returns:** [Decoder](_decoder_.decoder.md)<`A`> -___ - - -### `` optional - -▸ **optional**A(decoder: *[Decoder](_decoder_.decoder.md)<`A`>*): [Decoder](_decoder_.decoder.md)< `undefined` | `A`> - -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 = object({ - id: number(), - isOwner: optional(boolean()) -}); -``` - -**Type parameters:** - -#### A -**Parameters:** - -| Param | Type | -| ------ | ------ | -| decoder | [Decoder](_decoder_.decoder.md)<`A`> | - -**Returns:** [Decoder](_decoder_.decoder.md)< `undefined` | `A`> - ___ @@ -812,18 +753,18 @@ 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. +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 value to be `undefined` when appropriate. ``` -const optionalDecoder = valueAt(['a', 'b', 'c'], optional(string())); +const decoder = valueAt(['a', 'b', 'c'], union(string(), constant(undefined))); -optionalDecoder.run({a: {b: {c: 'surprise!'}}}) +decoder.run({a: {b: {c: 'surprise!'}}}) // => {ok: true, result: 'surprise!'} -optionalDecoder.run({a: {b: 'cats'}}) +decoder.run({a: {b: 'cats'}}) // => {ok: false, error: {... at: 'input.a.b.c' message: 'expected an object, got "cats"'} -optionalDecoder.run({a: {b: {z: 1}}}) +decoder.run({a: {b: {z: 1}}}) // => {ok: true, result: undefined} ``` diff --git a/docs/classes/_decoder_.optionaldecoder.md b/docs/classes/_decoder_.optionaldecoder.md new file mode 100644 index 0000000..b235b8a --- /dev/null +++ b/docs/classes/_decoder_.optionaldecoder.md @@ -0,0 +1,144 @@ +[@mojotech/json-type-validation](../README.md) > ["decoder"](../modules/_decoder_.md) > [OptionalDecoder](../classes/_decoder_.optionaldecoder.md) + +# Class: OptionalDecoder + +The `optional` decoder is given it's own type, the `OptionalDecoder` type, since it behaves differently from the other decoders. This decoder has no `run` method, so it can't be directly used to test a value. Instead, the `object` decoder accepts `optional` for decoding object properties that have been marked as optional with the `field?: value` notation. + +## Type parameters +#### A +## Hierarchy + +**OptionalDecoder** + +## Index + +### Properties + +* [decode](_decoder_.optionaldecoder.md#decode) + +### Methods + +* [andThen](_decoder_.optionaldecoder.md#andthen) +* [map](_decoder_.optionaldecoder.md#map) +* [optional](_decoder_.optionaldecoder.md#optional) + +--- + +## Properties + + + +### `` decode + +**● decode**: *`function`* + +#### Type declaration +▸(json: *`any`*): `Result.Result`< `A` | `undefined`, `Partial`<[DecoderError](../interfaces/_decoder_.decodererror.md)>> + +**Parameters:** + +| Param | Type | +| ------ | ------ | +| json | `any` | + +**Returns:** `Result.Result`< `A` | `undefined`, `Partial`<[DecoderError](../interfaces/_decoder_.decodererror.md)>> + +___ + +## Methods + + + +### andThen + +▸ **andThen**B(f: *`function`*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`> + +See `Decoder.prototype.andThen`. The function `f` is only executed if the optional decoder successfuly finds and decodes a value. + +**Type parameters:** + +#### B +**Parameters:** + +| Param | Type | +| ------ | ------ | +| f | `function` | + +**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`> + +___ + + +### map + +▸ **map**B(f: *`function`*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`> + +See `Decoder.prototype.map`. The function `f` is only executed if the optional decoder successfuly finds and decodes a value. + +**Type parameters:** + +#### B +**Parameters:** + +| Param | Type | +| ------ | ------ | +| f | `function` | + +**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`> + +___ + + +### `` optional + +▸ **optional**A(decoder: *[Decoder](_decoder_.decoder.md)<`A`>*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`A`> + +Decoder to designate that a property may not be present in an object. The behavior of `optional` is distinct from using `constant(undefined)` in that when the property is not found in the input, the key will not be present in the decoded value. + +Example: + +``` +// type with explicit undefined property +interface Breakfast1 { + eggs: number; + withBacon: boolean | undefined; +} + +// type with optional property +interface Breakfast2 { + eggs: number; + withBacon?: boolean; +} + +// in the first case we can't use `optional` +breakfast1Decoder = object({ + eggs: number(), + withBacon: union(boolean(), constant(undefined)) +}); + +// in the second case we can +breakfast2Decoder = object({ + eggs: number(), + withBacon: optional(boolean()) +}); + +breakfast1Decoder.run({eggs: 12}) +// => {ok: true, result: {eggs: 12, withBacon: undefined}} + +breakfast2Decoder.run({eggs: 7}) +// => {ok: true, result: {eggs: 7}} +``` + +**Type parameters:** + +#### A +**Parameters:** + +| Param | Type | +| ------ | ------ | +| decoder | [Decoder](_decoder_.decoder.md)<`A`> | + +**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`A`> + +___ + diff --git a/docs/modules/_combinators_.md b/docs/modules/_combinators_.md index b472855..561727d 100644 --- a/docs/modules/_combinators_.md +++ b/docs/modules/_combinators_.md @@ -11,6 +11,7 @@ * [dict](_combinators_.md#dict) * [fail](_combinators_.md#fail) * [lazy](_combinators_.md#lazy) +* [object](_combinators_.md#object) * [oneOf](_combinators_.md#oneof) * [optional](_combinators_.md#optional) * [succeed](_combinators_.md#succeed) @@ -22,7 +23,6 @@ * [boolean](_combinators_.md#boolean) * [constant](_combinators_.md#constant) * [number](_combinators_.md#number) -* [object](_combinators_.md#object) * [string](_combinators_.md#string) * [union](_combinators_.md#union) @@ -135,6 +135,15 @@ See `Decoder.lazy` **Returns:** [Decoder](../classes/_decoder_.decoder.md)<`A`> +___ + + +### `` object + +**● object**: *[object](../classes/_decoder_.decoder.md#object)* = Decoder.object + +See `Decoder.object` + ___ @@ -163,24 +172,10 @@ ___ ### `` optional -**● optional**: *`function`* = Decoder.optional +**● optional**: *[optional]()* = OptionalDecoder.optional See `Decoder.optional` -#### Type declaration -▸A(decoder: *[Decoder](../classes/_decoder_.decoder.md)<`A`>*): [Decoder](../classes/_decoder_.decoder.md)< `A` | `undefined`> - -**Type parameters:** - -#### A -**Parameters:** - -| Param | Type | -| ------ | ------ | -| decoder | [Decoder](../classes/_decoder_.decoder.md)<`A`> | - -**Returns:** [Decoder](../classes/_decoder_.decoder.md)< `A` | `undefined`> - ___ @@ -319,26 +314,6 @@ See `Decoder.number` **Returns:** [Decoder](../classes/_decoder_.decoder.md)<`number`> -___ - - -### object - -▸ **object**A(decoders: *[DecoderObject](_decoder_.md#decoderobject)<`A`>*): [Decoder](../classes/_decoder_.decoder.md)<`A`> - -See `Decoder.object` - -**Type parameters:** - -#### A -**Parameters:** - -| Param | Type | -| ------ | ------ | -| decoders | [DecoderObject](_decoder_.md#decoderobject)<`A`> | - -**Returns:** [Decoder](../classes/_decoder_.decoder.md)<`A`> - ___ diff --git a/docs/modules/_decoder_.md b/docs/modules/_decoder_.md index dc101d2..47d220e 100644 --- a/docs/modules/_decoder_.md +++ b/docs/modules/_decoder_.md @@ -7,6 +7,7 @@ ### Classes * [Decoder](../classes/_decoder_.decoder.md) +* [OptionalDecoder](../classes/_decoder_.optionaldecoder.md) ### Interfaces @@ -31,19 +32,25 @@ **ΤDecoderObject**: *`object`* -Defines a mapped type over an interface `A`. `DecoderObject` is an interface that has all the keys or `A`, but each key's property type is mapped to a decoder for that type. This type is used when creating decoders for objects. +Defines a mapped type over an interface `A`. This type is used when creating decoders for objects. + +`DecoderObject` is an interface that has all the properties or `A`, but each property's type is mapped to a decoder for that type. If a property is required in `A`, the decoder type is `Decoder`. If a property is optional in `A`, then that property is required in `DecoderObject`, but the decoder type is `OptionalDecoder | Decoder`. + +The `OptionalDecoder` type is only returned by the `optional` decoder. Example: ``` -interface X { +interface ABC { a: boolean; - b: string; + b?: string; + c: number | undefined; } -const decoderObject: DecoderObject = { - a: boolean(), - b: string() +DecoderObject === { + a: Decoder; + b: OptionalDecoder | Decoder; + c: Decoder; } ```