true-myth
Version:
A library for safe functional programming in JavaScript, with first-class support for TypeScript
182 lines (152 loc) • 5.97 kB
JavaScript
/**
Provides useful integrations between True Myth’s {@linkcode Result} and
{@linkcode Task} types and any library that implements [Standard Schema][ss].
[ss]: https://standardschema.dev
@module
*/
import * as result from './result.js';
import * as task from './task.js';
/**
Create a synchronous parser for unknown data to use with any library that
implements Standard Schema (Zod, Arktype, Valibot, etc.).
The resulting parser will accept `unknown` data and emit a {@linkcode Result},
which will be {@linkcode result.Ok Ok} if the schema successfully validates,
or a {@linkcode result.Err Err} with the {@linkcode StandardSchemaV1.Issue
Issue}s generated by the schema for invalid data.
## Examples
Creating a parser with Zod:
```ts
import { parserFor } from 'true-myth/standard-schema';
import * as z from 'zod';
interface Person {
name?: string | undefined;
age: number;
}
const parsePerson = parserFor(z.object({
name: z.string().optional(),
age: z.number().nonnegative(),
}));
```
Creating a parser with Arktype:
```ts
import { parserFor } from 'true-myth/standard-schema';
import { type } from 'arktype';
interface Person {
name?: string | undefined;
age: number;
}
const parsePerson = parserFor(type({
'name?': 'string',
age: 'number>=0',
}));
```
Other libraries work similarly!
Once you have a parser, you can simply call it with any value and then use the
normal {@linkcode Result} APIs.
```ts
parsePerson({ name: "Old!", age: 112 }).match({
Ok: (person) => {
console.log(`${person.name ?? "someone"} is ${person.age} years old.`);
},
Err: (error) => {
console.error("Something is wrong!", ...error.issues);
}
});
```
## Throws
The parser created by `parserFor` will throw an {@linkcode InvalidAsyncSchema}
error if the schema it was created from produces an async result, i.e., a
`Promise`. Standard Schema is [currently unable][gh] to distinguish between
synchronous and asynchronous parsers due to limitations in Zod.
If you need to handle schemas which may throw, use {@linkcode asyncParserFor}
instead. It will safely lift *all* results into a {@linkcode Task}, which you
can then safely interact with asynchronously as usual.
[gh]: https://github.com/standard-schema/standard-schema/issues/22
*/
export function parserFor(schema) {
return (data) => {
let schemaResult = schema['~standard'].validate(data);
if (schemaResult instanceof Promise) {
throw new InvalidAsyncSchema();
}
return isSuccess(schemaResult) ? result.ok(schemaResult.value) : result.err(schemaResult);
};
}
/**
An error thrown when calling a parser created with `parserFor` produces a
`Promise` instance.
*/
class InvalidAsyncSchema extends Error {
name = 'InvalidAsyncSchema';
constructor() {
super('Invalid use of an async schema with `parserFor`');
}
}
// Some libraries (Valibot, for one!) do not correctly return an object with
// no `value` (incorrectly distinguishing between not having the field and
// having the field with an empty object!), so we need to check instead if
// there are issues present instead.
function isSuccess(sr) {
const hasIssues = 'issues' in sr && sr.issues != null && sr.issues.length > 0;
return 'value' in sr && !hasIssues;
}
/**
Create an asynchronous parser for unknown data to use with any library that
implements Standard Schema (Zod, Arktype, Valibot, etc.).
The resulting parser will accept `unknown` data and emit a {@linkcode Task},
which will be {@linkcode task.Resolved Resolved} if the schema successfully
validates, or {@linkcode task.Rejected Rejected} with the {@linkcode
StandardSchemaV1.Issue Issue}s generated by the schema for invalid data.
If passed a parser that produces results synchronously, this function will
lift it into a {@linkcode Task}.
## Examples
With Zod:
```ts
import { asyncParserFor } from 'true-myth/standard-schema';
import * as z from 'zod';
interface Person {
name?: string | undefined;
age: number;
}
const parsePerson = asyncParserFor(z.object({
name: z.optional(z.string()),
// Define an async refinement so we have something to work with. This is a
// placeholder for some kind of *real* async validation you might do!
age: z.number().refine(async (val) => val >= 0),
}));
```
Other libraries that support async validation or transformation work similarly
(but not all libraries support this).
Once you have a parser, you can simply call it with any value and then use the
normal {@linkcode Task} APIs.
```ts
await parsePerson({ name: "Old!", age: 112 }).match({
Resolved: (person) => {
console.log(`${person.name ?? "someone"} is ${person.age} years old.`);
},
Rejected: (error) => {
console.error("Something is wrong!", ...error.issues);
}
});
```
@param schema A Standard Schema-compatible schema that produces a result,
possibly asynchronously.
@returns A {@linkcode Task} that resolves to the output of the schema when it
parses successfully and rejects with the `StandardSchema` `FailureResult`
when it fails to parse.
*/
export function asyncParserFor(schema) {
return (data) => {
let standardSchemaResult = schema['~standard'].validate(data);
let parseTask = standardSchemaResult instanceof Promise
? task.fromPromise(standardSchemaResult, () => {
/* v8 ignore next 2 */
throw new Error('Standard Schema should never throw an error');
})
: task.resolve(standardSchemaResult);
return parseTask.andThen((standardSchemaResult) => isSuccess(standardSchemaResult)
? task.resolve(standardSchemaResult.value)
: task.reject(standardSchemaResult));
};
}
//# sourceMappingURL=standard-schema.js.map