Skip to content

Commit

Permalink
Adds support for isPayload (#37)
Browse files Browse the repository at this point in the history
* Adds support for isPayload

* Fixes twoslashes
  • Loading branch information
arcanis committed Jun 30, 2023
1 parent 74d562a commit 1d5db13
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -2,7 +2,7 @@
"name": "typanion",
"description": "Simple runtime TypeScript validator library",
"homepage": "https://mael.dev/typanion/",
"version": "3.12.1",
"version": "3.13.0",
"main": "sources/index",
"license": "MIT",
"sideEffects": false,
Expand Down
37 changes: 37 additions & 0 deletions sources/predicates/typePredicates.ts
Expand Up @@ -208,6 +208,43 @@ export function isNumber() {
});
}

/**
* Important: This validator only makes sense when used in conjunction with
* coercion! It will always error when used without.
*
* Create a validator that only returns true when the tested value is a
* JSON representation of the expected type. Refines the type to the
* expected type, and casts the value into its inner value.
*/
export function isPayload<T extends AnyStrictValidator>(spec: T) {
return makeValidator<unknown, InferType<T>>({
test: (value, state): value is InferType<T> => {
if (typeof state?.coercions === `undefined`)
return pushError(state, `The isPayload predicate can only be used with coercion enabled`);

if (typeof state.coercion === `undefined`)
return pushError(state, `Unbound coercion result`);

if (typeof value !== `string`)
return pushError(state, `Expected a string (got ${getPrintable(value)})`);

let inner: unknown;
try {
inner = JSON.parse(value);
} catch {
return pushError(state, `Expected a JSON string (got ${getPrintable(value)})`);
}

const wrapper = {value: inner};
if (!spec(inner, {...state, coercion: makeCoercionFn(wrapper, `value`)}))
return false;

state.coercions.push([state.p ?? `.`, state.coercion.bind(null, wrapper.value)]);
return true;
},
});
}

/**
* Create a validator that only returns true when the tested value is a
* valid date. Refines the type to `Date`.
Expand Down
9 changes: 9 additions & 0 deletions tests/index.test.ts
Expand Up @@ -662,6 +662,15 @@ const COERCION_TESTS: {
[[[`val`, 42]], [], {val: 42}],
[[[`val`, 42, 12]], [`.[0]: Expected to have a length of exactly 2 elements (got 3)`]],
]
}, {
validator: () => t.isPayload(t.isObject({foo: t.isBoolean()})),
tests: [
[`{"foo": true}`, [], {foo: true}],
[`{"foo": 1}`, [], {foo: true}],
[`{"foo": 0}`, [], {foo: false}],
[`{"foo": "true"}`, [], {foo: true}],
[`{"foo": "false"}`, [], {foo: false}],
],
}];

describe(`Coercion Tests`, () => {
Expand Down
18 changes: 9 additions & 9 deletions website/docs/predicates/cascading.md
Expand Up @@ -23,9 +23,9 @@ import * as t from 'typanion';
declare const forbiddenKey1: string;
declare const forbiddenKey2: string;
declare const forbiddenKeyN: Array<string>;
declare const options: { missingIf: t.MissingType }
declare const options: {missingIf: t.MissingType};
// ---cut---
const validate = t.hasForbiddenKeys([forbiddenKey1, forbiddenKey2, ...forbiddenKeyN], options?);
const validate = t.hasForbiddenKeys([forbiddenKey1, forbiddenKey2, ...forbiddenKeyN], options);
```

Ensure that the objects don't contain any of the specified keys. (cf [`hasMutuallyExclusiveKeys`](#hasMutuallyExclusiveKeys) for the `options` parameter)
Expand Down Expand Up @@ -71,9 +71,9 @@ Ensure that the values all have a `length` property at least equal to the specif
```ts twoslash
import * as t from 'typanion';
declare const keys: Array<string>;
declare const options: { missingIf: t.MissingType }
declare const options: {missingIf: t.MissingType};
// ---cut---
const validate = t.hasMutuallyExclusiveKeys(keys, options?);
const validate = t.hasMutuallyExclusiveKeys(keys, options);
```

Ensure that the objects don't contain more than one of the specified keys. Keys will be considered missing based on `options.missingIf`.
Expand All @@ -90,9 +90,9 @@ Options:
```ts twoslash
import * as t from 'typanion';
declare const keys: Array<string>;
declare const options: { missingIf: t.MissingType }
declare const options: {missingIf: t.MissingType};
// ---cut---
const validate = t.hasRequiredKeys(keys, options?);
const validate = t.hasRequiredKeys(keys, options);
```

Ensure that the objects contain all of the specified keys. (cf [`hasMutuallyExclusiveKeys`](#hasMutuallyExclusiveKeys) for the `options` parameter)
Expand All @@ -102,9 +102,9 @@ Ensure that the objects contain all of the specified keys. (cf [`hasMutuallyExcl
```ts twoslash
import * as t from 'typanion';
declare const keys: Array<string>;
declare const options: { missingIf: t.MissingType }
declare const options: {missingIf: t.MissingType};
// ---cut---
const validate = t.hasAtLeastOneKey(keys, options?);
const validate = t.hasAtLeastOneKey(keys, options);
```

Ensure that the objects contain at least one of the specified keys. (cf [`hasMutuallyExclusiveKeys`](#hasMutuallyExclusiveKeys) for the `options` parameter)
Expand Down Expand Up @@ -179,7 +179,7 @@ Ensure that the values are round safe integers (enabling `unsafe` will allow [un
const validate = t.isJSON(schema?);
```
Ensure that the values are valid JSON, and optionally match them against a nested schema.
Ensure that the values are valid JSON, and optionally match them against a nested schema. Because it's a cascading predicate, it has no bearing on the type inference, and as a result doesn't support coercion. For a JSON predicate that supports coercion, check [`isPayload`](types.md#isPayload).
## `isLowerCase`
Expand Down
10 changes: 9 additions & 1 deletion website/docs/predicates/types.md
Expand Up @@ -21,7 +21,7 @@ Ensure that the values are all booleans. Prefer `isLiteral` if you wish to speci

## `isDate`

```
```ts
const validate = t.isDate();
```

Expand Down Expand Up @@ -89,6 +89,14 @@ const validate = t.isPartial(props);

Same as `isObject`, but allows any number of extraneous properties.

## `isPayload`

```ts
const validate = t.isPayload(spec);
```

Ensure that the values are JSON string whose parsed representation match the given validator. Unlike [`isJSON`](cascading.md#isJSON), `isPayload` will coerce the original value to match the nested spec. The drawback, however, is that it can only be used if the coercion is enabled, as it will otherwise always fail (that's because if it were to pass, then the resulting type refinement would be incorrect).

## `isSet`

```ts
Expand Down

0 comments on commit 1d5db13

Please sign in to comment.