Skip to content

Commit 945d416

Browse files
committed
Make the json family readonly
1 parent 0d86fac commit 945d416

File tree

6 files changed

+51
-25
lines changed

6 files changed

+51
-25
lines changed

src/core/Decoder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export interface Decoder<T> {
115115
* > be covered more elegantly by `.transform()`, `.refine()`, or `.pipe()`
116116
* > instead._
117117
*/
118-
then<V>(next: Next<V, T>, forceFlags?: number): Decoder<V>; // XXX This should not be public API
118+
then<V>(next: Next<V, T>, forceFlags?: number): Decoder<V>; // XXX This should not be public API
119119

120120
/**
121121
* Send the output of this decoder as input to another decoder.
@@ -362,7 +362,7 @@ export function define<T>(fn: AcceptanceFn<T>, flags = NONE): Decoder<T> {
362362
// message instead
363363
return err(annotate(result.error, message));
364364
}
365-
});
365+
}, flags);
366366
}
367367

368368
return brand({
@@ -412,13 +412,13 @@ export function defineReadonly<T>(
412412
/**
413413
* Ensures that the given decoder is read-only.
414414
*/
415-
export function readonly<T>(decoder: ReadonlyDecoder<T>): ReadonlyDecoder<T> {
415+
export function readonly<T>(decoder: Decoder<T>): ReadonlyDecoder<T> {
416416
if (!decoder.isReadonly) {
417417
const err = new Error('The provided decoder must be read-only');
418418
Error.captureStackTrace(err, readonly);
419419
throw err;
420420
}
421-
return decoder;
421+
return decoder as ReadonlyDecoder<T>;
422422
}
423423

424424
/** @internal */

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { date, datelike, iso8601 } from './dates';
88
export type { JSONArray, JSONObject, JSONValue } from './json';
99
export { json, jsonArray, jsonObject } from './json';
1010
export type { SizeOptions } from './lib/size-options';
11+
export type { LazyOptions } from './misc';
1112
export { instanceOf, lazy, prep } from './misc';
1213
export { anyNumber, integer, number, positiveInteger, positiveNumber } from './numbers';
1314
export { bigint } from './numbers';

src/json.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import type { Decoder } from '~/core';
1+
import type { ReadonlyDecoder } from '~/core';
22

33
import { array } from './arrays';
4-
import { null_ } from './basics';
54
import { boolean } from './booleans';
6-
import { record } from './collections';
7-
import { lazy } from './misc';
5+
import { either } from './unions';
6+
import { null_ } from './basics';
87
import { number } from './numbers';
8+
import { lazy } from './misc';
9+
import { record } from './collections';
910
import { string } from './strings';
10-
import { either } from './unions';
1111

1212
export type JSONValue = null | string | number | boolean | JSONObject | JSONArray;
1313
export type JSONObject = { [key: string]: JSONValue | undefined };
@@ -16,12 +16,16 @@ export type JSONArray = JSONValue[];
1616
/**
1717
* Accepts objects that contain only valid JSON values.
1818
*/
19-
export const jsonObject: Decoder<JSONObject> = lazy(() => record(json));
19+
export const jsonObject: ReadonlyDecoder<JSONObject> = lazy(() => record(json), {
20+
readonly: true,
21+
});
2022

2123
/**
2224
* Accepts arrays that contain only valid JSON values.
2325
*/
24-
export const jsonArray: Decoder<JSONArray> = lazy(() => array(json));
26+
export const jsonArray: ReadonlyDecoder<JSONArray> = lazy(() => array(json), {
27+
readonly: true,
28+
});
2529

2630
/**
2731
* Accepts any value that's a valid JSON value.
@@ -39,7 +43,7 @@ export const jsonArray: Decoder<JSONArray> = lazy(() => array(json));
3943
* | JSONValue[]
4044
* ```
4145
*/
42-
export const json: Decoder<JSONValue> = either(
46+
export const json: ReadonlyDecoder<JSONValue> = either(
4347
null_,
4448
string,
4549
number,

src/misc.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Decoder, ReadonlyDecoder } from '~/core';
2-
import { annotate, define, defineReadonly } from '~/core';
2+
import { annotate, define, defineReadonly, NONE, READONLY, readonly } from '~/core';
33

44
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
55
export interface Klass<T> extends Function {
@@ -20,12 +20,30 @@ export function instanceOf<K extends Klass<any>>(klass: K): ReadonlyDecoder<Inst
2020
);
2121
}
2222

23+
export type LazyOptions = {
24+
/** Ensure that the provided factory function may only return readonly decoders. */
25+
readonly: boolean;
26+
};
27+
2328
/**
2429
* Lazily evaluate the given decoder. This is useful to build self-referential
2530
* types for recursive data structures.
2631
*/
27-
export function lazy<T>(decoderFn: () => Decoder<T>): Decoder<T> {
28-
return define((blob) => decoderFn().decode(blob));
32+
export function lazy<T>(
33+
decoderFn: () => ReadonlyDecoder<T>,
34+
options: { readonly: true },
35+
): ReadonlyDecoder<T>;
36+
export function lazy<T>(decoderFn: () => Decoder<T>): Decoder<T>;
37+
export function lazy<T>(decoderFn: () => Decoder<T>, options?: LazyOptions): Decoder<T> {
38+
return define(
39+
(blob) => {
40+
let decoder = options?.readonly
41+
? readonly(decoderFn() as ReadonlyDecoder<T>)
42+
: decoderFn();
43+
return decoder.decode(blob);
44+
},
45+
options?.readonly ? READONLY : NONE,
46+
);
2947
}
3048

3149
/**

src/unions.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,20 @@ function nest(errText: string): string {
5454
* first one that accepts the input "wins". If all decoders reject the input,
5555
* the input gets rejected.
5656
*/
57-
export function either<TDecoders extends readonly Decoder<unknown>[]>(
58-
...decoders: TDecoders
59-
): Decoder<DecoderType<TDecoders[number]>> {
57+
export function either<RDs extends readonly ReadonlyDecoder<unknown>[]>(
58+
...decoders: RDs
59+
): ReadonlyDecoder<DecoderType<RDs[number]>>;
60+
export function either<Ds extends readonly Decoder<unknown>[]>(
61+
...decoders: Ds
62+
): Decoder<DecoderType<Ds[number]>>;
63+
export function either<Ds extends readonly Decoder<unknown>[]>(
64+
...decoders: Ds
65+
): Decoder<DecoderType<Ds[number]>> {
6066
if (decoders.length === 0) {
6167
throw new Error('Pass at least one decoder to either()');
6268
}
6369

64-
type T = DecoderType<TDecoders[number]>;
70+
type T = DecoderType<Ds[number]>;
6571
return define<T>(
6672
(blob, _, err) => {
6773
// Collect errors here along the way

test/json.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ describe('decoder', () => {
5656
expect(() => decoder.verify({ a: new Date() })).toThrow();
5757
});
5858

59-
// XXX Implement
60-
test.fails('readonliness', () => {
59+
test('readonliness', () => {
6160
expect(decoder.isReadonly).toBe(true);
6261
});
6362
});
@@ -96,8 +95,7 @@ describe('jsonObject', () => {
9695
expect(() => decoder.verify({ a: new Date() })).toThrow();
9796
});
9897

99-
// XXX Implement
100-
test.fails('readonliness', () => {
98+
test('readonliness', () => {
10199
expect(decoder.isReadonly).toBe(true);
102100
});
103101
});
@@ -134,8 +132,7 @@ describe('jsonArray', () => {
134132
expect(() => decoder.verify({ a: new Date() })).toThrow();
135133
});
136134

137-
// XXX Implement
138-
test.fails('readonliness', () => {
135+
test('readonliness', () => {
139136
expect(decoder.isReadonly).toBe(true);
140137
});
141138
});

0 commit comments

Comments
 (0)