From fd00028e833b5e5c4c3766a956ade4b7ce2cf50e Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 28 Jan 2022 14:26:44 +0000 Subject: [PATCH] Support readonly and/or non-empty arrays --- docs/modules/Codec.ts.md | 39 ++++++++++++++ docs/modules/Decoder.ts.md | 33 ++++++++++++ docs/modules/Encoder.ts.md | 35 ++++++++++++ dtslint/ts3.5/Codec.ts | 21 ++++++++ dtslint/ts3.5/Decoder.ts | 21 ++++++++ dtslint/ts3.5/Encoder.ts | 15 ++++++ src/Codec.ts | 28 ++++++++++ src/Decoder.ts | 25 +++++++++ src/Encoder.ts | 22 ++++++++ test/Codec.ts | 108 +++++++++++++++++++++++++++++++++++++ test/Decoder.ts | 81 ++++++++++++++++++++++++++++ test/Encoder.ts | 15 ++++++ 12 files changed, 443 insertions(+) diff --git a/docs/modules/Codec.ts.md b/docs/modules/Codec.ts.md index 0f5feca2..c7639d72 100644 --- a/docs/modules/Codec.ts.md +++ b/docs/modules/Codec.ts.md @@ -33,9 +33,12 @@ Added in v2.2.3 - [intersect](#intersect) - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) + - [nonEmptyArray](#nonemptyarray) - [nullable](#nullable) - [partial](#partial) - [readonly](#readonly) + - [readonlyArray](#readonlyarray) + - [readonlyNonEmptyArray](#readonlynonemptyarray) - [record](#record) - [refine](#refine) - [struct](#struct) @@ -216,6 +219,18 @@ export declare const mapLeftWithInput: ( Added in v2.2.3 +## nonEmptyArray + +**Signature** + +```ts +export declare function nonEmptyArray( + item: Codec +): Codec, NonEmptyArray> +``` + +Added in v2.2.17 + ## nullable **Signature** @@ -248,6 +263,30 @@ export declare const readonly: (codec: Codec) => Codec( + item: Codec +): Codec, ReadonlyArray> +``` + +Added in v2.2.17 + +## readonlyNonEmptyArray + +**Signature** + +```ts +export declare function readonlyNonEmptyArray( + item: Codec +): Codec, ReadonlyNonEmptyArray> +``` + +Added in v2.2.17 + ## record **Signature** diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md index 347edd76..55467e2e 100644 --- a/docs/modules/Decoder.ts.md +++ b/docs/modules/Decoder.ts.md @@ -43,10 +43,13 @@ Added in v2.2.7 - [intersect](#intersect) - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) + - [nonEmptyArray](#nonemptyarray) - [nullable](#nullable) - [parse](#parse) - [partial](#partial) - [readonly](#readonly) + - [readonlyArray](#readonlyarray) + - [readonlyNonEmptyArray](#readonlynonemptyarray) - [record](#record) - [refine](#refine) - [struct](#struct) @@ -297,6 +300,16 @@ export declare const mapLeftWithInput: ( Added in v2.2.7 +## nonEmptyArray + +**Signature** + +```ts +export declare const nonEmptyArray: (item: Decoder) => Decoder> +``` + +Added in v2.2.17 + ## nullable **Signature** @@ -341,6 +354,26 @@ export declare const readonly: (decoder: Decoder) => Decoder(item: Decoder) => Decoder +``` + +Added in v2.2.17 + +## readonlyNonEmptyArray + +**Signature** + +```ts +export declare const readonlyNonEmptyArray: (item: Decoder) => Decoder> +``` + +Added in v2.2.17 + ## record **Signature** diff --git a/docs/modules/Encoder.ts.md b/docs/modules/Encoder.ts.md index 153decb6..1ddbb707 100644 --- a/docs/modules/Encoder.ts.md +++ b/docs/modules/Encoder.ts.md @@ -29,9 +29,12 @@ Added in v2.2.3 - [array](#array) - [intersect](#intersect) - [lazy](#lazy) + - [nonEmptyArray](#nonemptyarray) - [nullable](#nullable) - [partial](#partial) - [readonly](#readonly) + - [readonlyArray](#readonlyarray) + - [readonlyNonEmptyArray](#readonlynonemptyarray) - [record](#record) - [struct](#struct) - [sum](#sum) @@ -118,6 +121,16 @@ export declare function lazy(f: () => Encoder): Encoder Added in v2.2.3 +## nonEmptyArray + +**Signature** + +```ts +export declare const nonEmptyArray: (item: Encoder) => Encoder, NonEmptyArray> +``` + +Added in v2.2.17 + ## nullable **Signature** @@ -150,6 +163,28 @@ export declare const readonly: (decoder: Encoder) => Encoder(item: Encoder) => Encoder +``` + +Added in v2.2.17 + +## readonlyNonEmptyArray + +**Signature** + +```ts +export declare const readonlyNonEmptyArray: ( + item: Encoder +) => Encoder, ReadonlyNonEmptyArray> +``` + +Added in v2.2.17 + ## record **Signature** diff --git a/dtslint/ts3.5/Codec.ts b/dtslint/ts3.5/Codec.ts index 677e7fe0..1733d21a 100644 --- a/dtslint/ts3.5/Codec.ts +++ b/dtslint/ts3.5/Codec.ts @@ -64,6 +64,27 @@ _.fromArray(NumberFromString) // $ExpectType Codec _.array(_.string) +// +// readonlyArray +// + +// $ExpectType Codec, ReadonlyArray> +_.readonlyArray(_.string) + +// +// nonEmptyArray +// + +// $ExpectType Codec, NonEmptyArray> +_.nonEmptyArray(_.string) + +// +// readonlyNonEmptyArray +// + +// $ExpectType Codec, ReadonlyNonEmptyArray> +_.readonlyNonEmptyArray(_.string) + // // fromRecord // diff --git a/dtslint/ts3.5/Decoder.ts b/dtslint/ts3.5/Decoder.ts index 94e80ca7..2d483f56 100644 --- a/dtslint/ts3.5/Decoder.ts +++ b/dtslint/ts3.5/Decoder.ts @@ -81,6 +81,27 @@ _.fromArray(NumberFromString) // $ExpectType Decoder _.array(_.string) +// +// readonlyArray +// + +// $ExpectType Decoder> +_.readonlyArray(_.string) + +// +// nonEmptyArray +// + +// $ExpectType Decoder> +_.nonEmptyArray(_.string) + +// +// readonlyNonEmptyArray +// + +// $ExpectType Decoder> +_.readonlyNonEmptyArray(_.string) + // // fromRecord // diff --git a/dtslint/ts3.5/Encoder.ts b/dtslint/ts3.5/Encoder.ts index 8bb901d4..2c2f0958 100644 --- a/dtslint/ts3.5/Encoder.ts +++ b/dtslint/ts3.5/Encoder.ts @@ -46,6 +46,21 @@ E.record(NumberToString) // $ExpectType Encoder, Record +// +// readonlyArray +// +E.readonlyArray(NumberToString) // $ExpectType Encoder, ReadonlyArray> + +// +// nonEmptyArray +// +E.nonEmptyArray(NumberToString) // $ExpectType Encoder, NonEmptyArray> + +// +// readonlyNonEmptyArray +// +E.readonlyNonEmptyArray(NumberToString) // $ExpectType Encoder, ReadonlyNonEmptyArray> + // // tuple // diff --git a/src/Codec.ts b/src/Codec.ts index fe43c2aa..05a97bcd 100644 --- a/src/Codec.ts +++ b/src/Codec.ts @@ -10,6 +10,8 @@ */ import { identity, Refinement } from 'fp-ts/lib/function' import { Invariant3 } from 'fp-ts/lib/Invariant' +import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray' +import { ReadonlyNonEmptyArray } from 'fp-ts/lib/ReadonlyNonEmptyArray' import { pipe } from 'fp-ts/lib/pipeable' import * as D from './Decoder' import * as E from './Encoder' @@ -220,6 +222,32 @@ export function array(item: Codec): Codec return pipe(UnknownArray, compose(fromArray(item))) as any } +/** + * @category combinators + * @since 2.2.17 + */ +export function readonlyArray(item: Codec): Codec, ReadonlyArray> { + return make(D.readonlyArray(item), E.readonlyArray(item)) +} + +/** + * @category combinators + * @since 2.2.17 + */ +export function nonEmptyArray(item: Codec): Codec, NonEmptyArray> { + return make(D.nonEmptyArray(item), E.nonEmptyArray(item)) +} + +/** + * @category combinators + * @since 2.2.17 + */ +export function readonlyNonEmptyArray( + item: Codec +): Codec, ReadonlyNonEmptyArray> { + return make(D.readonlyNonEmptyArray(item), E.readonlyNonEmptyArray(item)) +} + /** * @category combinators * @since 2.2.3 diff --git a/src/Decoder.ts b/src/Decoder.ts index f98ff1d2..8686c231 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -9,6 +9,10 @@ * @since 2.2.7 */ import { Alt2, Alt2C } from 'fp-ts/lib/Alt' +import * as A from 'fp-ts/lib/Array' +import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray' +import * as RA from 'fp-ts/lib/ReadonlyArray' +import { ReadonlyNonEmptyArray } from 'fp-ts/lib/ReadonlyNonEmptyArray' import { Bifunctor2 } from 'fp-ts/lib/Bifunctor' import { Category2 } from 'fp-ts/lib/Category' import * as E from 'fp-ts/lib/Either' @@ -292,6 +296,27 @@ export const fromArray = (item: Decoder): Decoder, Array export const array = (item: Decoder): Decoder> => pipe(UnknownArray, compose(fromArray(item))) +/** + * @category combinators + * @since 2.2.17 + */ +export const readonlyArray = (item: Decoder): Decoder> => + pipe(array(item), readonly) + +/** + * @category combinators + * @since 2.2.17 + */ +export const nonEmptyArray = (item: Decoder): Decoder> => + pipe(array(item), refine(A.isNonEmpty, 'NonEmptyArray')) + +/** + * @category combinators + * @since 2.2.17 + */ +export const readonlyNonEmptyArray = (item: Decoder): Decoder> => + pipe(readonlyArray(item), refine(RA.isNonEmpty, 'ReadonlyNonEmptyArray')) + /** * @category combinators * @since 2.2.8 diff --git a/src/Encoder.ts b/src/Encoder.ts index 8cf7a085..6b75cf5a 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -10,6 +10,8 @@ */ import { Contravariant2 } from 'fp-ts/lib/Contravariant' import { Category2 } from 'fp-ts/lib/Category' +import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray' +import { ReadonlyNonEmptyArray } from 'fp-ts/lib/ReadonlyNonEmptyArray' import { memoize, intersect_ } from './Schemable' import { identity } from 'fp-ts/lib/function' @@ -115,6 +117,26 @@ export function array(item: Encoder): Encoder, Array> { } } +/** + * @category combinators + * @since 2.2.17 + */ +export const readonlyArray: (item: Encoder) => Encoder, ReadonlyArray> = array as any + +/** + * @category combinators + * @since 2.2.17 + */ +export const nonEmptyArray: (item: Encoder) => Encoder, NonEmptyArray> = array as any + +/** + * @category combinators + * @since 2.2.17 + */ +export const readonlyNonEmptyArray: ( + item: Encoder +) => Encoder, ReadonlyNonEmptyArray> = readonlyArray as any + /** * @category combinators * @since 2.2.3 diff --git a/test/Codec.ts b/test/Codec.ts index 8b228ee3..de78d080 100644 --- a/test/Codec.ts +++ b/test/Codec.ts @@ -445,6 +445,114 @@ describe('Codec', () => { }) }) + describe('readonlyArray', () => { + describe('decode', () => { + it('should decode a valid input', () => { + const codec = _.readonlyArray(_.string) + assert.deepStrictEqual(codec.decode([]), D.success([])) + assert.deepStrictEqual(codec.decode(['a']), D.success(['a'])) + }) + + it('should reject an invalid input', () => { + const codec = _.readonlyArray(_.string) + assert.deepStrictEqual(codec.decode(undefined), D.failure(undefined, 'Array')) + assert.deepStrictEqual(codec.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', () => { + const codec = _.readonlyArray(_.string) + assert.deepStrictEqual( + codec.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + + describe('encode', () => { + it('should encode a value', () => { + const codec = _.readonlyArray(codecNumber) + assert.deepStrictEqual(codec.encode([1, 2]), ['1', '2']) + }) + }) + }) + + describe('nonEmptyArray', () => { + describe('decode', () => { + it('should decode a valid input', () => { + const codec = _.nonEmptyArray(_.string) + assert.deepStrictEqual(codec.decode(['a']), D.success(['a'])) + }) + + it('should reject an invalid input', () => { + const codec = _.nonEmptyArray(_.string) + assert.deepStrictEqual(codec.decode(undefined), D.failure(undefined, 'Array')) + assert.deepStrictEqual(codec.decode([]), D.failure([], 'NonEmptyArray')) + assert.deepStrictEqual(codec.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', () => { + const codec = _.nonEmptyArray(_.string) + assert.deepStrictEqual( + codec.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + + describe('encode', () => { + it('should encode a value', () => { + const codec = _.nonEmptyArray(codecNumber) + assert.deepStrictEqual(codec.encode([1, 2]), ['1', '2']) + }) + }) + }) + + describe('readonlyNonEmptyArray', () => { + describe('decode', () => { + it('should decode a valid input', () => { + const codec = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual(codec.decode(['a']), D.success(['a'])) + }) + + it('should reject an invalid input', () => { + const codec = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual(codec.decode(undefined), D.failure(undefined, 'Array')) + assert.deepStrictEqual(codec.decode([]), D.failure([], 'ReadonlyNonEmptyArray')) + assert.deepStrictEqual(codec.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', () => { + const codec = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual( + codec.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + + describe('encode', () => { + it('should encode a value', () => { + const codec = _.readonlyNonEmptyArray(codecNumber) + assert.deepStrictEqual(codec.encode([1, 2]), ['1', '2']) + }) + }) + }) + describe('tuple', () => { describe('decode', () => { it('should decode a valid input', () => { diff --git a/test/Decoder.ts b/test/Decoder.ts index f1323521..c6b99ff2 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -291,6 +291,87 @@ describe('Decoder', () => { }) }) + describe('readonlyArray', () => { + it('should decode a valid input', async () => { + const decoder = _.readonlyArray(_.string) + assert.deepStrictEqual(decoder.decode([]), _.success([])) + assert.deepStrictEqual(decoder.decode(['a']), _.success(['a'])) + }) + + it('should reject an invalid input', async () => { + const decoder = _.readonlyArray(_.string) + assert.deepStrictEqual(decoder.decode(undefined), E.left(FS.of(DE.leaf(undefined, 'Array')))) + assert.deepStrictEqual(decoder.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', async () => { + const decoder = _.readonlyArray(_.string) + assert.deepStrictEqual( + decoder.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + + describe('nonEmptyArray', () => { + it('should decode a valid input', async () => { + const decoder = _.nonEmptyArray(_.string) + assert.deepStrictEqual(decoder.decode(['a']), _.success(['a'])) + }) + + it('should reject an invalid input', async () => { + const decoder = _.nonEmptyArray(_.string) + assert.deepStrictEqual(decoder.decode(undefined), E.left(FS.of(DE.leaf(undefined, 'Array')))) + assert.deepStrictEqual(decoder.decode([]), E.left(FS.of(DE.leaf([], 'NonEmptyArray')))) + assert.deepStrictEqual(decoder.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', async () => { + const decoder = _.nonEmptyArray(_.string) + assert.deepStrictEqual( + decoder.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + + describe('readonlyNonEmptyArray', () => { + it('should decode a valid input', async () => { + const decoder = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual(decoder.decode(['a']), _.success(['a'])) + }) + + it('should reject an invalid input', async () => { + const decoder = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual(decoder.decode(undefined), E.left(FS.of(DE.leaf(undefined, 'Array')))) + assert.deepStrictEqual(decoder.decode([]), E.left(FS.of(DE.leaf([], 'ReadonlyNonEmptyArray')))) + assert.deepStrictEqual(decoder.decode([1]), E.left(FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))))) + }) + + it('should collect all errors', async () => { + const decoder = _.readonlyNonEmptyArray(_.string) + assert.deepStrictEqual( + decoder.decode([1, 2]), + E.left( + FS.concat( + FS.of(DE.index(0, DE.optional, FS.of(DE.leaf(1, 'string')))), + FS.of(DE.index(1, DE.optional, FS.of(DE.leaf(2, 'string')))) + ) + ) + ) + }) + }) + describe('record', () => { it('should decode a valid value', async () => { const decoder = _.record(_.number) diff --git a/test/Encoder.ts b/test/Encoder.ts index d4155a63..c8602839 100644 --- a/test/Encoder.ts +++ b/test/Encoder.ts @@ -44,6 +44,21 @@ describe('Encoder', () => { assert.deepStrictEqual(encoder.encode([1, 2]), ['1', '2']) }) + it('readonlyArray', () => { + const encoder = E.readonlyArray(H.encoderNumberToString) + assert.deepStrictEqual(encoder.encode([1, 2]), ['1', '2']) + }) + + it('nonEmptyArray', () => { + const encoder = E.nonEmptyArray(H.encoderNumberToString) + assert.deepStrictEqual(encoder.encode([1, 2]), ['1', '2']) + }) + + it('readonlyNonEmptyArray', () => { + const encoder = E.readonlyNonEmptyArray(H.encoderNumberToString) + assert.deepStrictEqual(encoder.encode([1, 2]), ['1', '2']) + }) + it('tuple', () => { const encoder = E.tuple(H.encoderNumberToString, H.encoderBooleanToNumber) assert.deepStrictEqual(encoder.encode([3, true]), ['3', 1])