nalloc 0.2.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -178
- package/build/codemod-cli.cjs +153 -0
- package/build/codemod-cli.cjs.map +1 -0
- package/build/codemod-cli.d.ts +2 -0
- package/build/codemod-cli.js +103 -0
- package/build/codemod-cli.js.map +1 -0
- package/build/codemod.cjs +652 -0
- package/build/codemod.cjs.map +1 -0
- package/build/codemod.d.ts +29 -0
- package/build/codemod.js +634 -0
- package/build/codemod.js.map +1 -0
- package/build/eslint.cjs +221 -0
- package/build/eslint.cjs.map +1 -0
- package/build/eslint.d.ts +36 -0
- package/build/eslint.js +198 -0
- package/build/eslint.js.map +1 -0
- package/build/http.cjs +31 -0
- package/build/http.cjs.map +1 -0
- package/build/http.d.ts +31 -0
- package/build/http.js +13 -0
- package/build/http.js.map +1 -0
- package/build/nonempty.cjs +35 -0
- package/build/nonempty.cjs.map +1 -0
- package/build/nonempty.d.ts +34 -0
- package/build/nonempty.js +14 -0
- package/build/nonempty.js.map +1 -0
- package/build/option.cjs +1 -1
- package/build/option.cjs.map +1 -1
- package/build/option.d.ts +2 -2
- package/build/option.js +1 -1
- package/build/option.js.map +1 -1
- package/build/result.cjs +10 -18
- package/build/result.cjs.map +1 -1
- package/build/result.d.ts +3 -34
- package/build/result.js +10 -15
- package/build/result.js.map +1 -1
- package/build/safe.cjs +8 -0
- package/build/safe.cjs.map +1 -1
- package/build/safe.d.ts +3 -0
- package/build/safe.js +2 -0
- package/build/safe.js.map +1 -1
- package/build/schema.cjs +32 -0
- package/build/schema.cjs.map +1 -0
- package/build/schema.d.ts +44 -0
- package/build/schema.js +14 -0
- package/build/schema.js.map +1 -0
- package/package.json +63 -10
- package/src/__tests__/codemod.ts +211 -0
- package/src/__tests__/eslint.ts +99 -0
- package/src/__tests__/fixtures/tsconfig.json +10 -0
- package/src/__tests__/http.ts +64 -0
- package/src/__tests__/iter.ts +18 -0
- package/src/__tests__/nonempty.ts +46 -0
- package/src/__tests__/nonempty.types.ts +38 -0
- package/src/__tests__/option.ts +4 -0
- package/src/__tests__/result.ts +104 -129
- package/src/__tests__/result.types.ts +2 -2
- package/src/__tests__/schema.ts +58 -0
- package/src/codemod-cli.ts +108 -0
- package/src/codemod.ts +623 -0
- package/src/eslint.ts +145 -0
- package/src/http.ts +42 -0
- package/src/nonempty.ts +48 -0
- package/src/option.ts +3 -4
- package/src/result.ts +18 -49
- package/src/safe.ts +3 -0
- package/src/schema.ts +52 -0
package/src/eslint.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import type * as ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
interface RuleDocs {
|
|
5
|
+
description: string;
|
|
6
|
+
requiresTypeChecking?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const createRule = ESLintUtils.RuleCreator<RuleDocs>((name) => `https://github.com/3axap4eHko/nalloc#${name}`);
|
|
10
|
+
|
|
11
|
+
const UNWRAP_NAMES: ReadonlySet<string> = new Set(['unwrap', 'unwrapErr', 'expect', 'expectErr']);
|
|
12
|
+
const DEFAULT_MODULES: readonly string[] = ['nalloc', 'nalloc/safe', 'nalloc/unsafe', 'nalloc/result', 'nalloc/option'];
|
|
13
|
+
const DEFAULT_TYPE_NAMES: readonly string[] = ['Result', 'Ok', 'Err', 'Option', 'Some', 'None'];
|
|
14
|
+
|
|
15
|
+
function nallocTypeName(type: ts.Type, names: ReadonlySet<string>): string | undefined {
|
|
16
|
+
const direct = type.aliasSymbol?.getName();
|
|
17
|
+
if (direct !== undefined && names.has(direct)) {
|
|
18
|
+
return direct;
|
|
19
|
+
}
|
|
20
|
+
if (type.isUnion()) {
|
|
21
|
+
for (const member of type.types) {
|
|
22
|
+
const name = member.aliasSymbol?.getName();
|
|
23
|
+
if (name !== undefined && names.has(name)) {
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mustUse = createRule<[{ typeNames?: string[] }], 'mustUse'>({
|
|
32
|
+
name: 'must-use',
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Require Result and Option values to be handled instead of silently discarded',
|
|
37
|
+
requiresTypeChecking: true,
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
mustUse: 'This {{name}} value is discarded. Handle it with match/unwrapOr/isErr/isNone, return it, or assign it.',
|
|
41
|
+
},
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { typeNames: { type: 'array', items: { type: 'string' }, uniqueItems: true } },
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
defaultOptions: [{}],
|
|
50
|
+
},
|
|
51
|
+
create(context, [options]) {
|
|
52
|
+
const names = new Set(options.typeNames ?? DEFAULT_TYPE_NAMES);
|
|
53
|
+
const services = ESLintUtils.getParserServices(context);
|
|
54
|
+
const check = (node: TSESTree.Expression): void => {
|
|
55
|
+
const name = nallocTypeName(services.getTypeAtLocation(node), names);
|
|
56
|
+
if (name !== undefined) {
|
|
57
|
+
context.report({ node, messageId: 'mustUse', data: { name } });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
'ExpressionStatement > CallExpression'(node: TSESTree.CallExpression): void {
|
|
62
|
+
check(node);
|
|
63
|
+
},
|
|
64
|
+
'ExpressionStatement > AwaitExpression'(node: TSESTree.AwaitExpression): void {
|
|
65
|
+
check(node);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const noUnwrap = createRule<[{ modules?: string[] }], 'noUnwrap'>({
|
|
72
|
+
name: 'no-unwrap',
|
|
73
|
+
meta: {
|
|
74
|
+
type: 'suggestion',
|
|
75
|
+
docs: {
|
|
76
|
+
description: 'Disallow unwrap and expect on Result and Option, which throw on failure',
|
|
77
|
+
},
|
|
78
|
+
messages: {
|
|
79
|
+
noUnwrap: '{{name}} throws on failure. Handle the error with match/unwrapOr/isErr, or turn this rule off in test files.',
|
|
80
|
+
},
|
|
81
|
+
schema: [
|
|
82
|
+
{
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: { modules: { type: 'array', items: { type: 'string' }, uniqueItems: true } },
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
defaultOptions: [{}],
|
|
89
|
+
},
|
|
90
|
+
create(context, [options]) {
|
|
91
|
+
const modules = new Set(options.modules ?? DEFAULT_MODULES);
|
|
92
|
+
const namespaceLocals = new Set<string>();
|
|
93
|
+
const directLocals = new Map<string, string>();
|
|
94
|
+
return {
|
|
95
|
+
ImportDeclaration(node: TSESTree.ImportDeclaration): void {
|
|
96
|
+
if (typeof node.source.value !== 'string' || !modules.has(node.source.value)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const spec of node.specifiers) {
|
|
100
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
101
|
+
namespaceLocals.add(spec.local.name);
|
|
102
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
103
|
+
const imported = spec.imported.name;
|
|
104
|
+
if (imported === 'Result' || imported === 'Option') {
|
|
105
|
+
namespaceLocals.add(spec.local.name);
|
|
106
|
+
} else if (UNWRAP_NAMES.has(imported)) {
|
|
107
|
+
directLocals.set(spec.local.name, imported);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
CallExpression(node: TSESTree.CallExpression): void {
|
|
113
|
+
const callee = node.callee;
|
|
114
|
+
if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier' && callee.object.type === 'Identifier') {
|
|
115
|
+
if (UNWRAP_NAMES.has(callee.property.name) && namespaceLocals.has(callee.object.name)) {
|
|
116
|
+
context.report({ node: callee, messageId: 'noUnwrap', data: { name: callee.property.name } });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (callee.type === 'Identifier') {
|
|
121
|
+
const imported = directLocals.get(callee.name);
|
|
122
|
+
if (imported !== undefined) {
|
|
123
|
+
context.report({ node: callee, messageId: 'noUnwrap', data: { name: imported } });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const rules = { 'must-use': mustUse, 'no-unwrap': noUnwrap };
|
|
132
|
+
|
|
133
|
+
const plugin: { meta: { name: string }; rules: typeof rules; configs: Record<string, unknown> } = {
|
|
134
|
+
meta: { name: 'nalloc' },
|
|
135
|
+
rules,
|
|
136
|
+
configs: {},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
plugin.configs.recommended = {
|
|
140
|
+
plugins: { nalloc: plugin },
|
|
141
|
+
rules: { 'nalloc/must-use': 'error', 'nalloc/no-unwrap': 'error' },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export { mustUse, noUnwrap };
|
|
145
|
+
export default plugin;
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { err as ERR } from './types.js';
|
|
2
|
+
import type { Ok, Result } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a fetch Response into a Result, treating a non-ok status as an Err.
|
|
6
|
+
* Native fetch only rejects on transport errors, never on 4xx/5xx; this closes that gap.
|
|
7
|
+
* The failed Response itself is the error - read its status, headers, or body from it.
|
|
8
|
+
* @param response - The Response to inspect
|
|
9
|
+
* @returns Ok(response) when response.ok, Err(response) otherwise
|
|
10
|
+
* @example
|
|
11
|
+
* import { Result } from 'nalloc';
|
|
12
|
+
* import { fromResponse } from 'nalloc/http';
|
|
13
|
+
* const res = Result.flatMap(await Result.fromPromise(fetch(url)), fromResponse);
|
|
14
|
+
*/
|
|
15
|
+
export function fromResponse(response: Response): Result<Response, Response> {
|
|
16
|
+
return response.ok ? (response as Ok<Response>) : ERR(response);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs fetch and forces every failure mode into the error channel.
|
|
21
|
+
* Ok means the request connected AND returned a 2xx status; a non-2xx Response
|
|
22
|
+
* becomes Err(response), and a transport failure becomes Err with the thrown
|
|
23
|
+
* value (per spec: TypeError on network/CORS errors, DOMException on abort/timeout).
|
|
24
|
+
* The body is never read - it stays available to the caller.
|
|
25
|
+
* @param input - The fetch input (URL or Request)
|
|
26
|
+
* @param init - Optional fetch init
|
|
27
|
+
* @returns Promise of Ok(response) for 2xx, Err otherwise
|
|
28
|
+
* @example
|
|
29
|
+
* import { fromFetch } from 'nalloc/http';
|
|
30
|
+
* const res = await fromFetch(url);
|
|
31
|
+
* // Ok(Response) -> connected and 2xx
|
|
32
|
+
* // Err(Response) -> reached the server, non-2xx
|
|
33
|
+
* // Err(TypeError) -> network/CORS failure
|
|
34
|
+
* // Err(DOMException) -> aborted or timed out
|
|
35
|
+
*/
|
|
36
|
+
export async function fromFetch(input: string | URL | Request, init?: RequestInit): Promise<Result<Response, Response | TypeError | DOMException>> {
|
|
37
|
+
try {
|
|
38
|
+
return fromResponse(await fetch(input, init));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return ERR(error as TypeError | DOMException);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/nonempty.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NONE } from './types.js';
|
|
2
|
+
import type { Option, Some } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** An array proven to contain at least one element. The runtime value is a plain array. */
|
|
5
|
+
export type NonEmptyArray<T> = [T, ...T[]];
|
|
6
|
+
|
|
7
|
+
/** A readonly array proven to contain at least one element. The runtime value is a plain array. */
|
|
8
|
+
export type ReadonlyNonEmptyArray<T> = readonly [T, ...T[]];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks whether an array has at least one element.
|
|
12
|
+
* @param values - The array to check
|
|
13
|
+
* @returns true if the array is non-empty
|
|
14
|
+
* @example
|
|
15
|
+
* isNonEmpty([]) // false
|
|
16
|
+
* isNonEmpty([1]) // true
|
|
17
|
+
*/
|
|
18
|
+
export function isNonEmpty<T>(values: readonly T[]): values is ReadonlyNonEmptyArray<T> {
|
|
19
|
+
return values.length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Asserts that an array is non-empty, throwing otherwise.
|
|
24
|
+
* @param values - The array to check
|
|
25
|
+
* @param message - Custom error message
|
|
26
|
+
* @throws Error if the array is empty
|
|
27
|
+
* @example
|
|
28
|
+
* assertNonEmpty([1]) // passes
|
|
29
|
+
* assertNonEmpty([]) // throws Error
|
|
30
|
+
*/
|
|
31
|
+
export function assertNonEmpty<T>(values: readonly T[], message?: string): asserts values is ReadonlyNonEmptyArray<T> {
|
|
32
|
+
if (values.length === 0) {
|
|
33
|
+
throw new Error(message ?? 'Expected array to be non-empty');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts an array to an Option of a non-empty array. Returns the same array
|
|
39
|
+
* value when non-empty - no allocation or cloning.
|
|
40
|
+
* @param values - The array to convert
|
|
41
|
+
* @returns Some(values) typed as non-empty if length > 0, None otherwise
|
|
42
|
+
* @example
|
|
43
|
+
* fromArray([]) // None
|
|
44
|
+
* fromArray([1, 2]) // Some([1, 2]) with non-empty type
|
|
45
|
+
*/
|
|
46
|
+
export function fromArray<T>(values: readonly T[]): Option<ReadonlyNonEmptyArray<T>> {
|
|
47
|
+
return values.length > 0 ? (values as Some<ReadonlyNonEmptyArray<T>>) : NONE;
|
|
48
|
+
}
|
package/src/option.ts
CHANGED
|
@@ -77,15 +77,14 @@ export function assertSome<T>(opt: Option<T>, message?: string): asserts opt is
|
|
|
77
77
|
* runtime to preserve zero-allocation semantics. Use assertSome() if you
|
|
78
78
|
* need runtime validation that a value is Some.
|
|
79
79
|
*
|
|
80
|
-
* @param
|
|
80
|
+
* @param value - The value to assert as Option (not validated at runtime)
|
|
81
81
|
* @example
|
|
82
82
|
* const value: number | null = getValue();
|
|
83
83
|
* satisfiesOption(value); // Compiles, but no runtime check
|
|
84
84
|
* // value is now typed as Option<number>
|
|
85
85
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
86
|
+
// oxlint-disable-next-line no-unused-vars
|
|
87
|
+
export function satisfiesOption<T>(value: Option<T> | T): asserts value is Option<T> {}
|
|
89
88
|
|
|
90
89
|
/**
|
|
91
90
|
* Maps and filters an iterable, collecting only Some values.
|
package/src/result.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { err as ERR, isOk, isErr, isSome, isNone, NONE, optionOf, isThenable } from './types.js';
|
|
2
2
|
import type { Ok, Err, Result, Option, Widen, WidenNever, MaybePromise } from './types.js';
|
|
3
|
+
import type { NonEmptyArray } from './nonempty.js';
|
|
3
4
|
|
|
4
5
|
export type { Ok, Err, Result };
|
|
5
6
|
export { isOk, isErr };
|
|
@@ -105,43 +106,6 @@ export async function fromPromise<T, E = unknown>(promise: Promise<T>, onError?:
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
/** A validation issue from a Standard Schema validator. */
|
|
109
|
-
export interface SchemaIssue {
|
|
110
|
-
readonly message: string;
|
|
111
|
-
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Minimal Standard Schema v1 interface (duck-typed, no external dependency). */
|
|
115
|
-
export interface StandardSchema<O = unknown> {
|
|
116
|
-
readonly '~standard': {
|
|
117
|
-
readonly validate: (value: unknown) => StandardSchemaResult<O> | Promise<StandardSchemaResult<O>>;
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
type StandardSchemaResult<O> = { readonly value: O; readonly issues?: undefined } | { readonly issues: ReadonlyArray<SchemaIssue> };
|
|
122
|
-
|
|
123
|
-
function schemaResultToResult<O>(sr: StandardSchemaResult<O>): Result<O, readonly SchemaIssue[]> {
|
|
124
|
-
return sr.issues ? ERR(sr.issues) : (sr.value as Ok<O>);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Validates a value against a Standard Schema and returns a Result.
|
|
129
|
-
* Works with any Standard Schema v1 compliant library (Zod, Valibot, ArkType, etc.).
|
|
130
|
-
* Returns synchronously if the schema validates synchronously.
|
|
131
|
-
* @param schema - A Standard Schema v1 compliant schema
|
|
132
|
-
* @param value - The value to validate
|
|
133
|
-
* @returns Ok(parsed) if valid, Err(issues) if invalid
|
|
134
|
-
* @example
|
|
135
|
-
* import { z } from 'zod';
|
|
136
|
-
* const result = fromSchema(z.string().email(), input);
|
|
137
|
-
* // Result<string, readonly SchemaIssue[]>
|
|
138
|
-
*/
|
|
139
|
-
export function fromSchema<O>(schema: StandardSchema<O>, value: unknown): Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
140
|
-
const sr = schema['~standard'].validate(value);
|
|
141
|
-
if (isThenable(sr)) return sr.then(schemaResultToResult);
|
|
142
|
-
return schemaResultToResult(sr);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
109
|
/**
|
|
146
110
|
* Executes a function that may return sync or async, preserving sync execution when possible.
|
|
147
111
|
* @param fn - Function that may return T or Promise<T>
|
|
@@ -659,7 +623,7 @@ export function collect<T, E>(results: Result<T, E>[]): Result<T[], E> {
|
|
|
659
623
|
* collectAll([ok(1), ok(2)]) // Ok([1, 2])
|
|
660
624
|
* collectAll([ok(1), err('a'), err('b')]) // Err(['a', 'b'])
|
|
661
625
|
*/
|
|
662
|
-
export function collectAll<T, E>(results: Result<T, E>[]): Result<T[], E
|
|
626
|
+
export function collectAll<T, E>(results: Result<T, E>[]): Result<T[], NonEmptyArray<E>> {
|
|
663
627
|
const oks: T[] = [];
|
|
664
628
|
const errs: E[] = [];
|
|
665
629
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -670,7 +634,7 @@ export function collectAll<T, E>(results: Result<T, E>[]): Result<T[], E[]> {
|
|
|
670
634
|
errs.push(result.error);
|
|
671
635
|
}
|
|
672
636
|
}
|
|
673
|
-
return errs.length > 0 ? ERR(errs) : (oks as Ok<T[]>);
|
|
637
|
+
return errs.length > 0 ? ERR(errs as NonEmptyArray<E>) : (oks as Ok<T[]>);
|
|
674
638
|
}
|
|
675
639
|
|
|
676
640
|
/**
|
|
@@ -690,14 +654,14 @@ export function all<T, E>(results: Result<T, E>[]): Result<readonly Widen<T>[],
|
|
|
690
654
|
* any([err('a'), ok(1), err('b')]) // Ok(1)
|
|
691
655
|
* any([err('a'), err('b')]) // Err(['a', 'b'])
|
|
692
656
|
*/
|
|
693
|
-
export function any<T, E>(results: Result<T, E
|
|
657
|
+
export function any<T, E>(results: NonEmptyArray<Result<T, E>>): Result<Widen<T>, NonEmptyArray<WidenNever<E>>> {
|
|
694
658
|
const errors: WidenNever<E>[] = [];
|
|
695
659
|
for (let i = 0; i < results.length; i++) {
|
|
696
660
|
const result = results[i];
|
|
697
661
|
if (isOk(result)) return result as Ok<Widen<T>>;
|
|
698
662
|
errors.push((result as Err<WidenNever<E>>).error);
|
|
699
663
|
}
|
|
700
|
-
return ERR(errors);
|
|
664
|
+
return ERR(errors as NonEmptyArray<WidenNever<E>>);
|
|
701
665
|
}
|
|
702
666
|
|
|
703
667
|
/**
|
|
@@ -711,7 +675,7 @@ export function any<T, E>(results: Result<T, E>[]): Result<Widen<T>, WidenNever<
|
|
|
711
675
|
*/
|
|
712
676
|
export function transpose<T, E>(result: Result<Option<T>, E>): Option<Result<T, E>> {
|
|
713
677
|
if (isErr(result)) {
|
|
714
|
-
return
|
|
678
|
+
return result as Option<Result<T, E>>;
|
|
715
679
|
}
|
|
716
680
|
const opt = result as Option<T>;
|
|
717
681
|
return isNone(opt) ? NONE : (opt as unknown as Ok<T> as Option<Result<T, E>>);
|
|
@@ -909,11 +873,8 @@ export async function safeTryAsync<T>(fn: () => Promise<T>): Promise<Result<T, u
|
|
|
909
873
|
}
|
|
910
874
|
|
|
911
875
|
function* unwrapYield<T, E>(result: Result<T, E>): Generator<Err<E>, T> {
|
|
912
|
-
if (isErr(result))
|
|
913
|
-
|
|
914
|
-
return undefined as never;
|
|
915
|
-
}
|
|
916
|
-
return result;
|
|
876
|
+
if (isErr(result)) yield result;
|
|
877
|
+
return result as T;
|
|
917
878
|
}
|
|
918
879
|
|
|
919
880
|
type Unwrapper = <T, E>(result: Result<T, E>) => Generator<Err<E>, T>;
|
|
@@ -934,7 +895,11 @@ type Unwrapper = <T, E>(result: Result<T, E>) => Generator<Err<E>, T>;
|
|
|
934
895
|
export function gen<E, T>(fn: ($: Unwrapper) => Generator<Err<E>, T>): Result<T, E> {
|
|
935
896
|
const iter = fn(unwrapYield);
|
|
936
897
|
const step = iter.next();
|
|
937
|
-
if (!step.done)
|
|
898
|
+
if (!step.done) {
|
|
899
|
+
// short-circuit abandons the generator mid-body; resume it so try/finally cleanup runs
|
|
900
|
+
iter.return(undefined as never);
|
|
901
|
+
return step.value;
|
|
902
|
+
}
|
|
938
903
|
return step.value as Ok<T>;
|
|
939
904
|
}
|
|
940
905
|
|
|
@@ -952,6 +917,10 @@ export function gen<E, T>(fn: ($: Unwrapper) => Generator<Err<E>, T>): Result<T,
|
|
|
952
917
|
export async function genAsync<E, T>(fn: ($: Unwrapper) => AsyncGenerator<Err<E>, T>): Promise<Result<T, E>> {
|
|
953
918
|
const iter = fn(unwrapYield);
|
|
954
919
|
const step = await iter.next();
|
|
955
|
-
if (!step.done)
|
|
920
|
+
if (!step.done) {
|
|
921
|
+
// short-circuit abandons the generator mid-body; resume it so try/finally cleanup runs
|
|
922
|
+
await iter.return(undefined as never);
|
|
923
|
+
return step.value;
|
|
924
|
+
}
|
|
956
925
|
return step.value as Ok<T>;
|
|
957
926
|
}
|
package/src/safe.ts
CHANGED
|
@@ -16,9 +16,12 @@ export type {
|
|
|
16
16
|
MaybePromise,
|
|
17
17
|
} from './types.js';
|
|
18
18
|
export { safeTry, safeTryAsync, unwrap, gen, genAsync } from './result.js';
|
|
19
|
+
export type { NonEmptyArray, ReadonlyNonEmptyArray } from './nonempty.js';
|
|
19
20
|
export * as Option from './option.js';
|
|
20
21
|
export * as Result from './result.js';
|
|
21
22
|
export * as Iter from './iter.js';
|
|
23
|
+
export * as NonEmpty from './nonempty.js';
|
|
24
|
+
export * as Schema from './schema.js';
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Threads a value through a sequence of unary functions, left to right.
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { err as ERR, isThenable } from './types.js';
|
|
2
|
+
import type { Ok, Result } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** A validation issue from a Standard Schema validator. */
|
|
5
|
+
export interface SchemaIssue {
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Minimal Standard Schema v1 interface (duck-typed, no external dependency). */
|
|
11
|
+
export interface StandardSchema<O = unknown> {
|
|
12
|
+
readonly '~standard': {
|
|
13
|
+
readonly validate: (value: unknown) => StandardSchemaResult<O> | Promise<StandardSchemaResult<O>>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type StandardSchemaResult<O> = { readonly value: O; readonly issues?: undefined } | { readonly issues: ReadonlyArray<SchemaIssue> };
|
|
18
|
+
|
|
19
|
+
function schemaResultToResult<O>(sr: StandardSchemaResult<O>): Result<O, readonly SchemaIssue[]> {
|
|
20
|
+
return sr.issues ? ERR(sr.issues) : (sr.value as Ok<O>);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a value against a Standard Schema and returns a Result.
|
|
25
|
+
* Works with any Standard Schema v1 compliant library (Zod, Valibot, ArkType, etc.).
|
|
26
|
+
* Returns synchronously if the schema validates synchronously.
|
|
27
|
+
* @param schema - A Standard Schema v1 compliant schema
|
|
28
|
+
* @param value - The value to validate
|
|
29
|
+
* @returns Ok(parsed) if valid, Err(issues) if invalid
|
|
30
|
+
* @example
|
|
31
|
+
* import { z } from 'zod';
|
|
32
|
+
* const result = fromSchema(z.string().email(), input);
|
|
33
|
+
* // Result<string, readonly SchemaIssue[]>
|
|
34
|
+
*/
|
|
35
|
+
export function fromSchema<O>(schema: StandardSchema<O>, value: unknown): Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
36
|
+
const sr = schema['~standard'].validate(value);
|
|
37
|
+
if (isThenable(sr)) return sr.then(schemaResultToResult);
|
|
38
|
+
return schemaResultToResult(sr);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Binds a Standard Schema once and returns a reusable validator function.
|
|
43
|
+
* The partial-application form of fromSchema - validate many values against one schema.
|
|
44
|
+
* @param schema - A Standard Schema v1 compliant schema
|
|
45
|
+
* @returns A function that validates a value and returns a Result
|
|
46
|
+
* @example
|
|
47
|
+
* const parseUser = wrapSchema(userSchema);
|
|
48
|
+
* const result = parseUser(input); // Result<User, readonly SchemaIssue[]>
|
|
49
|
+
*/
|
|
50
|
+
export function wrapSchema<O>(schema: StandardSchema<O>): (value: unknown) => Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
51
|
+
return (value) => fromSchema(schema, value);
|
|
52
|
+
}
|