wellcrafted 0.32.0 → 0.33.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 +80 -55
- package/dist/error/index.d.ts +55 -90
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +41 -78
- package/dist/error/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,19 +54,22 @@ function getUser(id: UserId) { /* ... */ }
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
### 📋 Tagged Errors
|
|
57
|
-
Structured, serializable errors with a
|
|
57
|
+
Structured, serializable errors with a declarative API
|
|
58
58
|
```typescript
|
|
59
|
-
import {
|
|
59
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Structured error — fields are spread flat on the error object
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
const errors = defineErrors({
|
|
62
|
+
// Static error — no fields needed
|
|
63
|
+
ValidationError: () => ({
|
|
64
|
+
message: "Email is required",
|
|
65
|
+
}),
|
|
66
|
+
// Structured error — fields are spread flat on the error object
|
|
67
|
+
ApiError: (fields: { endpoint: string }) => ({
|
|
68
|
+
...fields,
|
|
69
|
+
message: `Request to ${fields.endpoint} failed`,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
const { ValidationError, ApiError } = errors;
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
### 🔄 Query Integration
|
|
@@ -109,13 +112,17 @@ npm install wellcrafted
|
|
|
109
112
|
|
|
110
113
|
```typescript
|
|
111
114
|
import { tryAsync } from "wellcrafted/result";
|
|
112
|
-
import {
|
|
115
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
113
116
|
|
|
114
|
-
// Define your
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// Define your errors declaratively
|
|
118
|
+
const errors = defineErrors({
|
|
119
|
+
ApiError: (fields: { endpoint: string }) => ({
|
|
120
|
+
...fields,
|
|
121
|
+
message: `Failed to fetch ${fields.endpoint}`,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const { ApiError, ApiErr } = errors;
|
|
125
|
+
type ApiError = InferError<typeof errors, "ApiError">;
|
|
119
126
|
|
|
120
127
|
// Wrap any throwing operation
|
|
121
128
|
const { data, error } = await tryAsync({
|
|
@@ -213,14 +220,20 @@ if (error) {
|
|
|
213
220
|
### Wrap Unsafe Operations
|
|
214
221
|
|
|
215
222
|
```typescript
|
|
216
|
-
|
|
217
|
-
const { ParseError, ParseErr } = createTaggedError("ParseError")
|
|
218
|
-
.withFields<{ input: string }>()
|
|
219
|
-
.withMessage(({ input }) => `Invalid JSON: ${input.slice(0, 50)}`);
|
|
223
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
220
224
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
// Define errors declaratively
|
|
226
|
+
const errors = defineErrors({
|
|
227
|
+
ParseError: (fields: { input: string }) => ({
|
|
228
|
+
...fields,
|
|
229
|
+
message: `Invalid JSON: ${fields.input.slice(0, 50)}`,
|
|
230
|
+
}),
|
|
231
|
+
NetworkError: (fields: { url: string }) => ({
|
|
232
|
+
...fields,
|
|
233
|
+
message: `Request to ${fields.url} failed`,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
const { ParseErr, NetworkErr } = errors;
|
|
224
237
|
|
|
225
238
|
// Synchronous
|
|
226
239
|
const result = trySync({
|
|
@@ -239,16 +252,19 @@ const result = await tryAsync({
|
|
|
239
252
|
|
|
240
253
|
```typescript
|
|
241
254
|
// 1. Service Layer - Pure business logic
|
|
242
|
-
import {
|
|
255
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
243
256
|
import { tryAsync, Result, Ok } from "wellcrafted/result";
|
|
244
257
|
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
const recorderErrors = defineErrors({
|
|
259
|
+
RecorderServiceError: (fields: { currentState?: string; permissions?: string }) => ({
|
|
260
|
+
...fields,
|
|
261
|
+
message: fields.permissions
|
|
262
|
+
? `Missing ${fields.permissions} permission`
|
|
263
|
+
: `Invalid recorder state: ${fields.currentState}`,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
const { RecorderServiceError, RecorderServiceErr } = recorderErrors;
|
|
267
|
+
type RecorderServiceError = InferError<typeof recorderErrors, "RecorderServiceError">;
|
|
252
268
|
|
|
253
269
|
export function createRecorderService() {
|
|
254
270
|
let isRecording = false;
|
|
@@ -367,8 +383,12 @@ const { data: parsed } = trySync({
|
|
|
367
383
|
|
|
368
384
|
### Propagation Pattern (May Fail)
|
|
369
385
|
```typescript
|
|
370
|
-
const
|
|
371
|
-
|
|
386
|
+
const parseErrors = defineErrors({
|
|
387
|
+
ParseError: () => ({
|
|
388
|
+
message: "Invalid JSON",
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
const { ParseErr } = parseErrors;
|
|
372
392
|
|
|
373
393
|
// When catch can return Err<E>, function returns Result<T, E>
|
|
374
394
|
const mayFail = trySync({
|
|
@@ -421,16 +441,18 @@ Based on real-world usage, here's the recommended pattern for creating services
|
|
|
421
441
|
### Factory Function Pattern
|
|
422
442
|
|
|
423
443
|
```typescript
|
|
424
|
-
import {
|
|
444
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
425
445
|
import { Result, Ok } from "wellcrafted/result";
|
|
426
446
|
|
|
427
447
|
// 1. Define service-specific errors with typed fields and message
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
isRecording ? "Already recording" : "Not currently recording"
|
|
432
|
-
)
|
|
433
|
-
|
|
448
|
+
const recorderErrors = defineErrors({
|
|
449
|
+
RecorderServiceError: (fields: { isRecording: boolean }) => ({
|
|
450
|
+
...fields,
|
|
451
|
+
message: fields.isRecording ? "Already recording" : "Not currently recording",
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
const { RecorderServiceError, RecorderServiceErr } = recorderErrors;
|
|
455
|
+
type RecorderServiceError = InferError<typeof recorderErrors, "RecorderServiceError">;
|
|
434
456
|
|
|
435
457
|
// 2. Create service with factory function
|
|
436
458
|
export function createRecorderService() {
|
|
@@ -534,12 +556,16 @@ export async function GET(request: Request) {
|
|
|
534
556
|
<summary><b>Form Validation</b></summary>
|
|
535
557
|
|
|
536
558
|
```typescript
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
import { defineErrors, type InferError } from "wellcrafted/error";
|
|
560
|
+
|
|
561
|
+
const formErrors = defineErrors({
|
|
562
|
+
FormError: (fields: { fields: Record<string, string[]> }) => ({
|
|
563
|
+
...fields,
|
|
564
|
+
message: `Validation failed for: ${Object.keys(fields.fields).join(", ")}`,
|
|
565
|
+
}),
|
|
566
|
+
});
|
|
567
|
+
const { FormErr } = formErrors;
|
|
568
|
+
type FormError = InferError<typeof formErrors, "FormError">;
|
|
543
569
|
|
|
544
570
|
function validateLoginForm(data: unknown): Result<LoginData, FormError> {
|
|
545
571
|
const errors: Record<string, string[]> = {};
|
|
@@ -617,20 +643,19 @@ For comprehensive examples, service layer patterns, framework integrations, and
|
|
|
617
643
|
- **`defineMutation(options)`** - Define a mutation with dual interface (`.options` + callable/`.execute()`)
|
|
618
644
|
|
|
619
645
|
### Error Functions
|
|
620
|
-
- **`
|
|
621
|
-
-
|
|
622
|
-
-
|
|
623
|
-
-
|
|
624
|
-
-
|
|
625
|
-
- `name` is a reserved key — prevented at compile time
|
|
626
|
-
- Factory input is flat (e.g., `{ endpoint: '/api' }`), not nested
|
|
646
|
+
- **`defineErrors(definitions)`** - Define multiple error factories in a single declaration
|
|
647
|
+
- Each key becomes an error name; the value is a factory function returning fields + `message`
|
|
648
|
+
- Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped) for each key
|
|
649
|
+
- Factory functions receive typed fields and return `{ ...fields, message }`
|
|
650
|
+
- No-field errors use `() => ({ message: '...' })`
|
|
651
|
+
- `name` is a reserved key — prevented at compile time
|
|
627
652
|
- **`extractErrorMessage(error)`** - Extract readable message from unknown error
|
|
628
653
|
|
|
629
654
|
### Types
|
|
630
655
|
- **`Result<T, E>`** - Union of Ok<T> | Err<E>
|
|
631
656
|
- **`Ok<T>`** - Success result type
|
|
632
657
|
- **`Err<E>`** - Error result type
|
|
633
|
-
- **`
|
|
658
|
+
- **`InferError<TErrors, TName>`** - Extract error type from `defineErrors` result
|
|
634
659
|
- **`Brand<T, B>`** - Branded type wrapper
|
|
635
660
|
- **`ExtractOkFromResult<R>`** - Extract Ok variant from Result union
|
|
636
661
|
- **`ExtractErrFromResult<R>`** - Extract Err variant from Result union
|
package/dist/error/index.d.ts
CHANGED
|
@@ -21,111 +21,76 @@ type AnyTaggedError = {
|
|
|
21
21
|
message: string;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @template TName - The error name (discriminator for tagged unions)
|
|
29
|
-
* @template TFields - Additional fields spread flat on the error (default: none)
|
|
24
|
+
* Constructor return must include `message: string`.
|
|
25
|
+
* JSON serializability is a convention, not enforced at the type level
|
|
26
|
+
* (optional fields produce `T | undefined` which breaks `JsonObject`).
|
|
30
27
|
*/
|
|
31
|
-
type
|
|
32
|
-
name: TName;
|
|
28
|
+
type ErrorBody = {
|
|
33
29
|
message: string;
|
|
34
|
-
} & TFields>;
|
|
35
|
-
//# sourceMappingURL=types.d.ts.map
|
|
36
|
-
//#endregion
|
|
37
|
-
//#region src/error/utils.d.ts
|
|
38
|
-
/**
|
|
39
|
-
* Extracts a readable error message from an unknown error value
|
|
40
|
-
*
|
|
41
|
-
* @param error - The unknown error to extract a message from
|
|
42
|
-
* @returns A string representation of the error
|
|
43
|
-
*/
|
|
44
|
-
declare function extractErrorMessage(error: unknown): string;
|
|
45
|
-
/**
|
|
46
|
-
* Constraint that prevents the reserved key `name` from appearing in fields.
|
|
47
|
-
* `message` is not reserved — when `.withMessage()` seals it, `message` is
|
|
48
|
-
* simply absent from the input type. When `.withMessage()` is not used,
|
|
49
|
-
* `message` is a built-in input handled separately from TFields.
|
|
50
|
-
*/
|
|
51
|
-
type NoReservedKeys = {
|
|
52
|
-
name?: never;
|
|
53
30
|
};
|
|
54
31
|
/**
|
|
55
|
-
*
|
|
32
|
+
* Per-key validation: tells the user exactly what `name` will be stamped as.
|
|
33
|
+
* If a user provides `name` in the return object, they see a descriptive error.
|
|
56
34
|
*/
|
|
57
|
-
type
|
|
58
|
-
/**
|
|
59
|
-
* Message template function type.
|
|
60
|
-
* Receives the fields (without name/message) and returns a message string.
|
|
61
|
-
*/
|
|
62
|
-
type MessageFn<TFields extends JsonObject> = (input: TFields) => string;
|
|
63
|
-
/** Factory input when message is required (no `.withMessage()`). */
|
|
64
|
-
type RequiredMessageInput<TFields extends JsonObject> = {
|
|
35
|
+
type ValidateErrorBody<K extends string> = {
|
|
65
36
|
message: string;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Whether the entire input parameter can be omitted (called with no args).
|
|
69
|
-
* True when TFields is empty or all-optional (and message is sealed via withMessage).
|
|
70
|
-
*/
|
|
71
|
-
type IsInputOptional<TFields extends JsonObject> = Partial<TFields> extends TFields ? true : false;
|
|
72
|
-
/**
|
|
73
|
-
* Factories returned by `.withMessage()` — message is sealed by the template.
|
|
74
|
-
* `message` is NOT in the input type. Only has factory functions, no chain methods.
|
|
75
|
-
*/
|
|
76
|
-
type SealedFactories<TName extends `${string}Error`, TFields extends JsonObject> = { [K in TName]: IsInputOptional<TFields> extends true ? (input?: TFields) => TaggedError<TName, TFields> : (input: TFields) => TaggedError<TName, TFields> } & { [K in ReplaceErrorWithErr<TName>]: IsInputOptional<TFields> extends true ? (input?: TFields) => Err<TaggedError<TName, TFields>> : (input: TFields) => Err<TaggedError<TName, TFields>> };
|
|
77
|
-
/**
|
|
78
|
-
* Builder object returned by createTaggedError (and .withFields()).
|
|
79
|
-
* Has factory functions (message required) AND chain methods.
|
|
80
|
-
*/
|
|
81
|
-
type ErrorBuilder<TName extends `${string}Error`, TFields extends JsonObject = Record<never, never>> = { [K in TName]: (input: RequiredMessageInput<TFields>) => TaggedError<TName, TFields> } & { [K in ReplaceErrorWithErr<TName>]: (input: RequiredMessageInput<TFields>) => Err<TaggedError<TName, TFields>> } & {
|
|
82
|
-
/** Defines additional fields spread flat on the error object. */
|
|
83
|
-
withFields<T extends JsonObject & NoReservedKeys>(): ErrorBuilder<TName, T>;
|
|
84
|
-
/**
|
|
85
|
-
* Seals the message — the template owns it entirely.
|
|
86
|
-
* `message` is NOT in the returned factory's input type.
|
|
87
|
-
*/
|
|
88
|
-
withMessage(fn: MessageFn<TFields>): SealedFactories<TName, TFields>;
|
|
37
|
+
name?: `The 'name' key is reserved as '${K}'. Remove it.`;
|
|
89
38
|
};
|
|
39
|
+
/** The config: each key is a variant name, each value is a constructor function. */
|
|
40
|
+
type ErrorsConfig = Record<string, (...args: any[]) => ErrorBody>;
|
|
41
|
+
/** Validates each config entry, injecting the key-specific `name` reservation message. */
|
|
42
|
+
type ValidatedConfig<T extends ErrorsConfig> = { [K in keyof T & string]: T[K] extends ((...args: infer A) => infer R) ? (...args: A) => R & ValidateErrorBody<K> : T[K] };
|
|
43
|
+
/** Single factory: takes constructor args, returns Err-wrapped error. */
|
|
44
|
+
type ErrorFactory<TName extends string, TFn extends (...args: any[]) => ErrorBody> = { [K in TName]: (...args: Parameters<TFn>) => Err<Readonly<{
|
|
45
|
+
name: TName;
|
|
46
|
+
} & ReturnType<TFn>>> };
|
|
47
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
48
|
+
/** Return type of `defineErrors`. Maps each config key to its factory. */
|
|
49
|
+
type DefineErrorsReturn<TConfig extends ErrorsConfig> = UnionToIntersection<{ [K in keyof TConfig & string]: ErrorFactory<K, TConfig[K]> }[keyof TConfig & string]>;
|
|
50
|
+
/** Extract the error type from a single factory. */
|
|
51
|
+
type InferError<T> = T extends ((...args: any[]) => Err<infer R>) ? R : never;
|
|
52
|
+
/** Extract union of ALL error types from a defineErrors return. */
|
|
53
|
+
type InferErrors<T> = { [K in keyof T]: T[K] extends ((...args: any[]) => Err<infer R>) ? R : never }[keyof T];
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/error/defineErrors.d.ts
|
|
90
56
|
/**
|
|
91
|
-
*
|
|
57
|
+
* Defines a set of typed error factories using Rust-style namespaced variants.
|
|
92
58
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
59
|
+
* Each key is a short variant name (the namespace provides context). Every
|
|
60
|
+
* factory returns `Err<...>` directly — ready for `trySync`/`tryAsync` catch
|
|
61
|
+
* handlers. The variant name is stamped as `name` on the error object.
|
|
95
62
|
*
|
|
96
|
-
*
|
|
97
|
-
* 1. **No `.withMessage()`**: `message` is required at the call site
|
|
98
|
-
* 2. **With `.withMessage()`**: message is sealed — NOT in the input type
|
|
99
|
-
*
|
|
100
|
-
* @example No fields, message required at call site
|
|
63
|
+
* @example
|
|
101
64
|
* ```ts
|
|
102
|
-
* const
|
|
103
|
-
*
|
|
104
|
-
*
|
|
65
|
+
* const HttpError = defineErrors({
|
|
66
|
+
* Connection: ({ cause }: { cause: string }) => ({
|
|
67
|
+
* message: `Failed to connect: ${cause}`,
|
|
68
|
+
* cause,
|
|
69
|
+
* }),
|
|
70
|
+
* Parse: ({ cause }: { cause: string }) => ({
|
|
71
|
+
* message: `Failed to parse: ${cause}`,
|
|
72
|
+
* cause,
|
|
73
|
+
* }),
|
|
74
|
+
* });
|
|
105
75
|
*
|
|
106
|
-
*
|
|
107
|
-
* ```ts
|
|
108
|
-
* const { RecorderBusyError } = createTaggedError('RecorderBusyError')
|
|
109
|
-
* .withMessage(() => 'A recording is already in progress');
|
|
110
|
-
* RecorderBusyError(); // message always: 'A recording is already in progress'
|
|
111
|
-
* ```
|
|
76
|
+
* type HttpError = InferErrors<typeof HttpError>;
|
|
112
77
|
*
|
|
113
|
-
*
|
|
114
|
-
* ```ts
|
|
115
|
-
* const { FsReadError } = createTaggedError('FsReadError')
|
|
116
|
-
* .withFields<{ path: string }>();
|
|
117
|
-
* FsReadError({ message: 'Failed to read', path: '/etc/config' });
|
|
78
|
+
* const result = HttpError.Connection({ cause: 'timeout' }); // Err<...>
|
|
118
79
|
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare function defineErrors<const TConfig extends ErrorsConfig>(config: TConfig & ValidatedConfig<TConfig>): DefineErrorsReturn<TConfig>;
|
|
82
|
+
//# sourceMappingURL=defineErrors.d.ts.map
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/error/extractErrorMessage.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* Extracts a readable error message from an unknown error value
|
|
119
87
|
*
|
|
120
|
-
* @
|
|
121
|
-
*
|
|
122
|
-
* const { ResponseError } = createTaggedError('ResponseError')
|
|
123
|
-
* .withFields<{ status: number }>()
|
|
124
|
-
* .withMessage(({ status }) => `HTTP ${status}`);
|
|
125
|
-
* ResponseError({ status: 404 }); // message: "HTTP 404"
|
|
126
|
-
* ```
|
|
88
|
+
* @param error - The unknown error to extract a message from
|
|
89
|
+
* @returns A string representation of the error
|
|
127
90
|
*/
|
|
128
|
-
declare function
|
|
91
|
+
declare function extractErrorMessage(error: unknown): string;
|
|
92
|
+
//# sourceMappingURL=extractErrorMessage.d.ts.map
|
|
93
|
+
|
|
129
94
|
//#endregion
|
|
130
|
-
export { AnyTaggedError, JsonObject, JsonValue,
|
|
95
|
+
export { AnyTaggedError, DefineErrorsReturn, ErrorBody, ErrorsConfig, InferError, InferErrors, JsonObject, JsonValue, ValidatedConfig, defineErrors, extractErrorMessage };
|
|
131
96
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/defineErrors.ts","../../src/error/extractErrorMessage.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/error/index.js
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { Err } from "../result-DnOm5ds5.js";
|
|
2
2
|
|
|
3
|
-
//#region src/error/
|
|
3
|
+
//#region src/error/defineErrors.ts
|
|
4
|
+
/**
|
|
5
|
+
* Defines a set of typed error factories using Rust-style namespaced variants.
|
|
6
|
+
*
|
|
7
|
+
* Each key is a short variant name (the namespace provides context). Every
|
|
8
|
+
* factory returns `Err<...>` directly — ready for `trySync`/`tryAsync` catch
|
|
9
|
+
* handlers. The variant name is stamped as `name` on the error object.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const HttpError = defineErrors({
|
|
14
|
+
* Connection: ({ cause }: { cause: string }) => ({
|
|
15
|
+
* message: `Failed to connect: ${cause}`,
|
|
16
|
+
* cause,
|
|
17
|
+
* }),
|
|
18
|
+
* Parse: ({ cause }: { cause: string }) => ({
|
|
19
|
+
* message: `Failed to parse: ${cause}`,
|
|
20
|
+
* cause,
|
|
21
|
+
* }),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* type HttpError = InferErrors<typeof HttpError>;
|
|
25
|
+
*
|
|
26
|
+
* const result = HttpError.Connection({ cause: 'timeout' }); // Err<...>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function defineErrors(config) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [name, ctor] of Object.entries(config)) result[name] = (...args) => {
|
|
32
|
+
const body = ctor(...args);
|
|
33
|
+
return Err(Object.freeze({
|
|
34
|
+
...body,
|
|
35
|
+
name
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/error/extractErrorMessage.ts
|
|
4
43
|
/**
|
|
5
44
|
* Extracts a readable error message from an unknown error value
|
|
6
45
|
*
|
|
@@ -34,83 +73,7 @@ function extractErrorMessage(error) {
|
|
|
34
73
|
}
|
|
35
74
|
return String(error);
|
|
36
75
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Creates a new tagged error type with a fluent builder API.
|
|
39
|
-
*
|
|
40
|
-
* Returns an object with factory functions immediately available (message required),
|
|
41
|
-
* plus `.withFields<T>()` and `.withMessage(fn)` for further configuration.
|
|
42
|
-
*
|
|
43
|
-
* Two mutually exclusive modes:
|
|
44
|
-
* 1. **No `.withMessage()`**: `message` is required at the call site
|
|
45
|
-
* 2. **With `.withMessage()`**: message is sealed — NOT in the input type
|
|
46
|
-
*
|
|
47
|
-
* @example No fields, message required at call site
|
|
48
|
-
* ```ts
|
|
49
|
-
* const { SimpleError, SimpleErr } = createTaggedError('SimpleError');
|
|
50
|
-
* SimpleErr({ message: 'Something went wrong' });
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* @example No fields, with sealed message
|
|
54
|
-
* ```ts
|
|
55
|
-
* const { RecorderBusyError } = createTaggedError('RecorderBusyError')
|
|
56
|
-
* .withMessage(() => 'A recording is already in progress');
|
|
57
|
-
* RecorderBusyError(); // message always: 'A recording is already in progress'
|
|
58
|
-
* ```
|
|
59
|
-
*
|
|
60
|
-
* @example With fields, message required
|
|
61
|
-
* ```ts
|
|
62
|
-
* const { FsReadError } = createTaggedError('FsReadError')
|
|
63
|
-
* .withFields<{ path: string }>();
|
|
64
|
-
* FsReadError({ message: 'Failed to read', path: '/etc/config' });
|
|
65
|
-
* ```
|
|
66
|
-
*
|
|
67
|
-
* @example With fields + sealed message (template computes from fields)
|
|
68
|
-
* ```ts
|
|
69
|
-
* const { ResponseError } = createTaggedError('ResponseError')
|
|
70
|
-
* .withFields<{ status: number }>()
|
|
71
|
-
* .withMessage(({ status }) => `HTTP ${status}`);
|
|
72
|
-
* ResponseError({ status: 404 }); // message: "HTTP 404"
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
function createTaggedError(name) {
|
|
76
|
-
const errName = name.replace(/Error$/, "Err");
|
|
77
|
-
const createBuilder = () => {
|
|
78
|
-
const errorConstructor = (input) => {
|
|
79
|
-
const { message,...fields } = input;
|
|
80
|
-
return {
|
|
81
|
-
name,
|
|
82
|
-
message,
|
|
83
|
-
...fields
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
const errConstructor = (input) => Err(errorConstructor(input));
|
|
87
|
-
return {
|
|
88
|
-
[name]: errorConstructor,
|
|
89
|
-
[errName]: errConstructor,
|
|
90
|
-
withFields() {
|
|
91
|
-
return createBuilder();
|
|
92
|
-
},
|
|
93
|
-
withMessage(fn) {
|
|
94
|
-
const sealedErrorConstructor = (input) => {
|
|
95
|
-
const fields = input ?? {};
|
|
96
|
-
const message = fn(fields);
|
|
97
|
-
return {
|
|
98
|
-
name,
|
|
99
|
-
message,
|
|
100
|
-
...fields
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
const sealedErrConstructor = (input) => Err(sealedErrorConstructor(input));
|
|
104
|
-
return {
|
|
105
|
-
[name]: sealedErrorConstructor,
|
|
106
|
-
[errName]: sealedErrConstructor
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
};
|
|
111
|
-
return createBuilder();
|
|
112
|
-
}
|
|
113
76
|
|
|
114
77
|
//#endregion
|
|
115
|
-
export {
|
|
78
|
+
export { defineErrors, extractErrorMessage };
|
|
116
79
|
//# sourceMappingURL=index.js.map
|
package/dist/error/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["
|
|
1
|
+
{"version":3,"file":"index.js","names":["config: TConfig & ValidatedConfig<TConfig>","result: Record<string, unknown>","error: unknown"],"sources":["../../src/error/defineErrors.ts","../../src/error/extractErrorMessage.ts"],"sourcesContent":["import { Err } from \"../result/result.js\";\nimport type {\n\tDefineErrorsReturn,\n\tErrorsConfig,\n\tValidatedConfig,\n} from \"./types.js\";\n\n/**\n * Defines a set of typed error factories using Rust-style namespaced variants.\n *\n * Each key is a short variant name (the namespace provides context). Every\n * factory returns `Err<...>` directly — ready for `trySync`/`tryAsync` catch\n * handlers. The variant name is stamped as `name` on the error object.\n *\n * @example\n * ```ts\n * const HttpError = defineErrors({\n * Connection: ({ cause }: { cause: string }) => ({\n * message: `Failed to connect: ${cause}`,\n * cause,\n * }),\n * Parse: ({ cause }: { cause: string }) => ({\n * message: `Failed to parse: ${cause}`,\n * cause,\n * }),\n * });\n *\n * type HttpError = InferErrors<typeof HttpError>;\n *\n * const result = HttpError.Connection({ cause: 'timeout' }); // Err<...>\n * ```\n */\nexport function defineErrors<const TConfig extends ErrorsConfig>(\n\tconfig: TConfig & ValidatedConfig<TConfig>,\n): DefineErrorsReturn<TConfig> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const [name, ctor] of Object.entries(config)) {\n\t\tresult[name] = (...args: unknown[]) => {\n\t\t\tconst body = (ctor as (...a: unknown[]) => Record<string, unknown>)(\n\t\t\t\t...args,\n\t\t\t);\n\t\t\treturn Err(Object.freeze({ ...body, name }));\n\t\t};\n\t}\n\n\treturn result as DefineErrorsReturn<TConfig>;\n}\n","/**\n * Extracts a readable error message from an unknown error value\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n */\nexport function extractErrorMessage(error: unknown): string {\n\t// Handle Error instances\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\n\t// Handle primitives\n\tif (typeof error === \"string\") return error;\n\tif (\n\t\ttypeof error === \"number\" ||\n\t\ttypeof error === \"boolean\" ||\n\t\ttypeof error === \"bigint\"\n\t)\n\t\treturn String(error);\n\tif (typeof error === \"symbol\") return error.toString();\n\tif (error === null) return \"null\";\n\tif (error === undefined) return \"undefined\";\n\n\t// Handle arrays\n\tif (Array.isArray(error)) return JSON.stringify(error);\n\n\t// Handle plain objects\n\tif (typeof error === \"object\") {\n\t\tconst errorObj = error as Record<string, unknown>;\n\n\t\t// Check common error properties\n\t\tconst messageProps = [\n\t\t\t\"message\",\n\t\t\t\"error\",\n\t\t\t\"description\",\n\t\t\t\"title\",\n\t\t\t\"reason\",\n\t\t\t\"details\",\n\t\t] as const;\n\t\tfor (const prop of messageProps) {\n\t\t\tif (prop in errorObj && typeof errorObj[prop] === \"string\") {\n\t\t\t\treturn errorObj[prop];\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to JSON stringification\n\t\ttry {\n\t\t\treturn JSON.stringify(error);\n\t\t} catch {\n\t\t\treturn String(error);\n\t\t}\n\t}\n\n\t// Final fallback\n\treturn String(error);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,aACfA,QAC8B;CAC9B,MAAMC,SAAkC,CAAE;AAE1C,MAAK,MAAM,CAAC,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,CAChD,QAAO,QAAQ,CAAC,GAAG,SAAoB;EACtC,MAAM,OAAO,AAAC,KACb,GAAG,KACH;AACD,SAAO,IAAI,OAAO,OAAO;GAAE,GAAG;GAAM;EAAM,EAAC,CAAC;CAC5C;AAGF,QAAO;AACP;;;;;;;;;;ACzCD,SAAgB,oBAAoBC,OAAwB;AAE3D,KAAI,iBAAiB,MACpB,QAAO,MAAM;AAId,YAAW,UAAU,SAAU,QAAO;AACtC,YACQ,UAAU,mBACV,UAAU,oBACV,UAAU,SAEjB,QAAO,OAAO,MAAM;AACrB,YAAW,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,iBAAqB,QAAO;AAGhC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAK,UAAU,MAAM;AAGtD,YAAW,UAAU,UAAU;EAC9B,MAAM,WAAW;EAGjB,MAAM,eAAe;GACpB;GACA;GACA;GACA;GACA;GACA;EACA;AACD,OAAK,MAAM,QAAQ,aAClB,KAAI,QAAQ,mBAAmB,SAAS,UAAU,SACjD,QAAO,SAAS;AAKlB,MAAI;AACH,UAAO,KAAK,UAAU,MAAM;EAC5B,QAAO;AACP,UAAO,OAAO,MAAM;EACpB;CACD;AAGD,QAAO,OAAO,MAAM;AACpB"}
|