wellcrafted 0.31.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 CHANGED
@@ -21,8 +21,8 @@ try {
21
21
  const { data, error } = await saveUser(user);
22
22
  if (error) {
23
23
  switch (error.name) {
24
- case "ValidationError":
25
- showToast(`Invalid ${error.context.field}`);
24
+ case "ValidationError":
25
+ showToast(`Invalid ${error.field}`);
26
26
  break;
27
27
  case "AuthError":
28
28
  redirectToLogin();
@@ -54,18 +54,22 @@ function getUser(id: UserId) { /* ... */ }
54
54
  ```
55
55
 
56
56
  ### 📋 Tagged Errors
57
- Structured, serializable errors with a fluent API
57
+ Structured, serializable errors with a declarative API
58
58
  ```typescript
59
- import { createTaggedError } from "wellcrafted/error";
59
+ import { defineErrors, type InferError } from "wellcrafted/error";
60
60
 
61
- // Minimal by default - only name and message
62
- const { ValidationError } = createTaggedError("ValidationError");
63
- ValidationError({ message: "Email is required" });
64
-
65
- // Chain to add context and cause when needed
66
- const { ApiError } = createTaggedError("ApiError")
67
- .withContext<{ endpoint: string }>()
68
- .withCause<NetworkError | undefined>();
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;
69
73
  ```
70
74
 
71
75
  ### 🔄 Query Integration
@@ -108,22 +112,22 @@ npm install wellcrafted
108
112
 
109
113
  ```typescript
110
114
  import { tryAsync } from "wellcrafted/result";
111
- import { createTaggedError, type AnyTaggedError } from "wellcrafted/error";
115
+ import { defineErrors, type InferError } from "wellcrafted/error";
112
116
 
113
- // Define your error with factory function
114
- const { ApiError, ApiErr } = createTaggedError("ApiError")
115
- .withContext<{ endpoint: string }>()
116
- .withCause<AnyTaggedError | undefined>();
117
- type ApiError = ReturnType<typeof ApiError>;
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">;
118
126
 
119
127
  // Wrap any throwing operation
120
128
  const { data, error } = await tryAsync({
121
129
  try: () => fetch('/api/user').then(r => r.json()),
122
- catch: (e) => ApiErr({
123
- message: "Failed to fetch user",
124
- context: { endpoint: '/api/user' },
125
- cause: { name: "FetchError", message: String(e) }
126
- })
130
+ catch: (e) => ApiErr({ endpoint: '/api/user' })
127
131
  });
128
132
 
129
133
  if (error) {
@@ -216,29 +220,31 @@ if (error) {
216
220
  ### Wrap Unsafe Operations
217
221
 
218
222
  ```typescript
219
- // Define errors with context and cause
220
- const { ParseError, ParseErr } = createTaggedError("ParseError")
221
- .withContext<{ input: string }>();
223
+ import { defineErrors, type InferError } from "wellcrafted/error";
222
224
 
223
- const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
224
- .withContext<{ url: string }>();
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;
225
237
 
226
238
  // Synchronous
227
239
  const result = trySync({
228
240
  try: () => JSON.parse(jsonString),
229
- catch: () => ParseErr({
230
- message: "Invalid JSON",
231
- context: { input: jsonString }
232
- })
241
+ catch: () => ParseErr({ input: jsonString })
233
242
  });
234
243
 
235
244
  // Asynchronous
236
245
  const result = await tryAsync({
237
246
  try: () => fetch(url),
238
- catch: () => NetworkErr({
239
- message: "Request failed",
240
- context: { url }
241
- })
247
+ catch: () => NetworkErr({ url })
242
248
  });
243
249
  ```
244
250
 
@@ -246,12 +252,19 @@ const result = await tryAsync({
246
252
 
247
253
  ```typescript
248
254
  // 1. Service Layer - Pure business logic
249
- import { createTaggedError } from "wellcrafted/error";
255
+ import { defineErrors, type InferError } from "wellcrafted/error";
250
256
  import { tryAsync, Result, Ok } from "wellcrafted/result";
251
257
 
252
- const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
253
- .withContext<{ currentState?: string; permissions?: string }>();
254
- type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
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">;
255
268
 
256
269
  export function createRecorderService() {
257
270
  let isRecording = false;
@@ -260,10 +273,7 @@ export function createRecorderService() {
260
273
  return {
261
274
  async startRecording(): Promise<Result<void, RecorderServiceError>> {
262
275
  if (isRecording) {
263
- return RecorderServiceErr({
264
- message: "Already recording",
265
- context: { currentState: 'recording' }
266
- });
276
+ return RecorderServiceErr({ currentState: 'recording' });
267
277
  }
268
278
 
269
279
  return tryAsync({
@@ -273,19 +283,13 @@ export function createRecorderService() {
273
283
  // ... recording setup
274
284
  isRecording = true;
275
285
  },
276
- catch: () => RecorderServiceErr({
277
- message: "Failed to start recording",
278
- context: { permissions: 'microphone' }
279
- })
286
+ catch: () => RecorderServiceErr({ permissions: 'microphone' })
280
287
  });
281
288
  },
282
289
 
283
290
  async stopRecording(): Promise<Result<Blob, RecorderServiceError>> {
284
291
  if (!isRecording) {
285
- return RecorderServiceErr({
286
- message: "Not currently recording",
287
- context: { currentState: 'idle' }
288
- });
292
+ return RecorderServiceErr({ currentState: 'idle' });
289
293
  }
290
294
 
291
295
  // Stop recording and return blob...
@@ -379,12 +383,17 @@ const { data: parsed } = trySync({
379
383
 
380
384
  ### Propagation Pattern (May Fail)
381
385
  ```typescript
382
- const { ParseError, ParseErr } = createTaggedError("ParseError");
386
+ const parseErrors = defineErrors({
387
+ ParseError: () => ({
388
+ message: "Invalid JSON",
389
+ }),
390
+ });
391
+ const { ParseErr } = parseErrors;
383
392
 
384
393
  // When catch can return Err<E>, function returns Result<T, E>
385
394
  const mayFail = trySync({
386
395
  try: () => JSON.parse(riskyJson),
387
- catch: () => ParseErr({ message: "Invalid JSON" })
396
+ catch: () => ParseErr()
388
397
  });
389
398
  // mayFail: Result<object, ParseError> - Must check for errors
390
399
  if (isOk(mayFail)) {
@@ -402,7 +411,7 @@ const smartParse = trySync({
402
411
  return Ok({}); // Return Ok<T> for fallback
403
412
  }
404
413
  // Propagate other errors
405
- return ParseErr({ message: "Parse failed" });
414
+ return ParseErr();
406
415
  }
407
416
  });
408
417
  // smartParse: Result<object, ParseError> - Mixed handling = Result type
@@ -432,13 +441,18 @@ Based on real-world usage, here's the recommended pattern for creating services
432
441
  ### Factory Function Pattern
433
442
 
434
443
  ```typescript
435
- import { createTaggedError } from "wellcrafted/error";
444
+ import { defineErrors, type InferError } from "wellcrafted/error";
436
445
  import { Result, Ok } from "wellcrafted/result";
437
446
 
438
- // 1. Define service-specific errors with typed context
439
- const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
440
- .withContext<{ isRecording: boolean }>();
441
- type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
447
+ // 1. Define service-specific errors with typed fields and message
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">;
442
456
 
443
457
  // 2. Create service with factory function
444
458
  export function createRecorderService() {
@@ -449,10 +463,7 @@ export function createRecorderService() {
449
463
  return {
450
464
  startRecording(): Result<void, RecorderServiceError> {
451
465
  if (isRecording) {
452
- return RecorderServiceErr({
453
- message: "Already recording",
454
- context: { isRecording }
455
- });
466
+ return RecorderServiceErr({ isRecording });
456
467
  }
457
468
 
458
469
  isRecording = true;
@@ -461,10 +472,7 @@ export function createRecorderService() {
461
472
 
462
473
  stopRecording(): Result<Blob, RecorderServiceError> {
463
474
  if (!isRecording) {
464
- return RecorderServiceErr({
465
- message: "Not currently recording",
466
- context: { isRecording }
467
- });
475
+ return RecorderServiceErr({ isRecording });
468
476
  }
469
477
 
470
478
  isRecording = false;
@@ -548,8 +556,16 @@ export async function GET(request: Request) {
548
556
  <summary><b>Form Validation</b></summary>
549
557
 
550
558
  ```typescript
551
- const { FormError, FormErr } = createTaggedError("FormError")
552
- .withContext<{ fields: Record<string, string[]> }>();
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">;
553
569
 
554
570
  function validateLoginForm(data: unknown): Result<LoginData, FormError> {
555
571
  const errors: Record<string, string[]> = {};
@@ -559,10 +575,7 @@ function validateLoginForm(data: unknown): Result<LoginData, FormError> {
559
575
  }
560
576
 
561
577
  if (Object.keys(errors).length > 0) {
562
- return FormErr({
563
- message: "Validation failed",
564
- context: { fields: errors }
565
- });
578
+ return FormErr({ fields: errors });
566
579
  }
567
580
 
568
581
  return Ok(data as LoginData);
@@ -630,19 +643,19 @@ For comprehensive examples, service layer patterns, framework integrations, and
630
643
  - **`defineMutation(options)`** - Define a mutation with dual interface (`.options` + callable/`.execute()`)
631
644
 
632
645
  ### Error Functions
633
- - **`createTaggedError(name)`** - Creates error factory functions with fluent API
634
- - Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped)
635
- - **Default**: minimal errors with only `name` and `message`
636
- - Chain `.withContext<T>()` to add typed context
637
- - Chain `.withCause<T>()` to add typed cause
638
- - Include `| undefined` in type to make property optional but typed
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
639
652
  - **`extractErrorMessage(error)`** - Extract readable message from unknown error
640
653
 
641
654
  ### Types
642
655
  - **`Result<T, E>`** - Union of Ok<T> | Err<E>
643
656
  - **`Ok<T>`** - Success result type
644
657
  - **`Err<E>`** - Error result type
645
- - **`TaggedError<T>`** - Structured error type
658
+ - **`InferError<TErrors, TName>`** - Extract error type from `defineErrors` result
646
659
  - **`Brand<T, B>`** - Branded type wrapper
647
660
  - **`ExtractOkFromResult<R>`** - Extract Ok variant from Result union
648
661
  - **`ExtractErrFromResult<R>`** - Extract Err variant from Result union
@@ -14,48 +14,74 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
14
14
  */
15
15
  type JsonObject = Record<string, JsonValue>;
16
16
  /**
17
- * Base type for any tagged error, used as a constraint for cause parameters.
17
+ * Base type for any tagged error, used as a minimum constraint.
18
18
  */
19
19
  type AnyTaggedError = {
20
20
  name: string;
21
21
  message: string;
22
22
  };
23
23
  /**
24
- * Helper type that adds a context property.
25
- * - When TContext is undefined (default): NO context property (explicit opt-in)
26
- * - When TContext includes undefined (e.g., `{ foo: string } | undefined`): context is OPTIONAL but typed
27
- * - When TContext is a specific type without undefined: context is REQUIRED with that exact type
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`).
28
27
  */
29
- type WithContext<TContext> = [TContext] extends [undefined] ? Record<never, never> : [undefined] extends [TContext] ? {
30
- context?: Exclude<TContext, undefined>;
31
- } : {
32
- context: TContext;
28
+ type ErrorBody = {
29
+ message: string;
33
30
  };
34
31
  /**
35
- * Helper type that adds a cause property.
36
- * - When TCause is undefined (default): NO cause property (explicit opt-in)
37
- * - When TCause includes undefined (e.g., `NetworkError | undefined`): cause is OPTIONAL, constrained
38
- * - When TCause is a specific type without undefined: cause is REQUIRED
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.
39
34
  */
40
- type WithCause<TCause> = [TCause] extends [undefined] ? Record<never, never> : [undefined] extends [TCause] ? {
41
- cause?: Exclude<TCause, undefined>;
42
- } : {
43
- cause: TCause;
35
+ type ValidateErrorBody<K extends string> = {
36
+ message: string;
37
+ name?: `The 'name' key is reserved as '${K}'. Remove it.`;
44
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
45
56
  /**
46
- * A tagged error type for type-safe error handling.
47
- * Uses the `name` property as a discriminator for tagged unions.
57
+ * Defines a set of typed error factories using Rust-style namespaced variants.
58
+ *
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.
48
62
  *
49
- * @template TName - The error name (discriminator for tagged unions)
50
- * @template TContext - Additional context data for the error (default: undefined = no context)
51
- * @template TCause - The type of error that caused this error (default: undefined = no cause)
63
+ * @example
64
+ * ```ts
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
+ * });
75
+ *
76
+ * type HttpError = InferErrors<typeof HttpError>;
77
+ *
78
+ * const result = HttpError.Connection({ cause: 'timeout' }); // Err<...>
79
+ * ```
52
80
  */
53
- type TaggedError<TName extends string = string, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = Readonly<{
54
- name: TName;
55
- message: string;
56
- } & WithContext<TContext> & WithCause<TCause>>;
81
+ declare function defineErrors<const TConfig extends ErrorsConfig>(config: TConfig & ValidatedConfig<TConfig>): DefineErrorsReturn<TConfig>;
82
+ //# sourceMappingURL=defineErrors.d.ts.map
57
83
  //#endregion
58
- //#region src/error/utils.d.ts
84
+ //#region src/error/extractErrorMessage.d.ts
59
85
  /**
60
86
  * Extracts a readable error message from an unknown error value
61
87
  *
@@ -63,98 +89,8 @@ type TaggedError<TName extends string = string, TContext extends JsonObject | un
63
89
  * @returns A string representation of the error
64
90
  */
65
91
  declare function extractErrorMessage(error: unknown): string;
66
- /**
67
- * Replaces the "Error" suffix with "Err" suffix in error type names.
68
- */
69
- type ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}Error` ? `${TBase}Err` : never;
70
- /**
71
- * Input provided to the message template function.
72
- * Contains everything the error will have except `message` (since that's what it computes).
73
- */
74
- type MessageInput<TName extends string, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = {
75
- name: TName;
76
- } & ([TContext] extends [undefined] ? Record<never, never> : {
77
- context: Exclude<TContext, undefined>;
78
- }) & ([TCause] extends [undefined] ? Record<never, never> : {
79
- cause: Exclude<TCause, undefined>;
80
- });
81
- /**
82
- * Message template function type.
83
- */
84
- type MessageFn<TName extends string, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = (input: MessageInput<TName, TContext, TCause>) => string;
85
- /**
86
- * Helper type that determines optionality based on whether T includes undefined.
87
- */
88
- type OptionalIfUndefined<T, TKey extends string> = undefined extends T ? { [K in TKey]?: Exclude<T, undefined> } : { [K in TKey]: T };
89
- /**
90
- * Input type for error factory functions.
91
- * `message` is optional (computed from template when omitted).
92
- * `context` and `cause` follow the same optionality rules as before.
93
- */
94
- type ErrorCallInput<TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = {
95
- message?: string;
96
- } & (TContext extends undefined ? Record<never, never> : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? Record<never, never> : OptionalIfUndefined<TCause, "cause">);
97
- /**
98
- * The final factories object returned by `.withMessage()`.
99
- * Has factory functions, NO chain methods.
100
- */
101
- type FinalFactories<TName extends `${string}Error`, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = { [K in TName]: (input: ErrorCallInput<TContext, TCause>) => TaggedError<TName, TContext, TCause> } & { [K in ReplaceErrorWithErr<TName>]: (input: ErrorCallInput<TContext, TCause>) => Err<TaggedError<TName, TContext, TCause>> };
102
- /**
103
- * Builder interface for the fluent createTaggedError API.
104
- * Has chain methods only, NO factory functions.
105
- * Must call `.withMessage(fn)` to get factories.
106
- */
107
- type ErrorBuilder<TName extends `${string}Error`, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = {
108
- /**
109
- * Constrains the context type for this error.
110
- * `.withContext<T>()` where T doesn't include undefined → context is **required**
111
- * `.withContext<T | undefined>()` → context is **optional** but typed when provided
112
- */
113
- withContext<T extends JsonObject | undefined = JsonObject | undefined>(): ErrorBuilder<TName, T, TCause>;
114
- /**
115
- * Constrains the cause type for this error.
116
- * `.withCause<T>()` where T doesn't include undefined → cause is **required**
117
- * `.withCause<T | undefined>()` → cause is **optional** but typed when provided
118
- */
119
- withCause<T extends AnyTaggedError | undefined = AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
120
- /**
121
- * Terminal method that defines how the error message is computed from its data.
122
- * Returns the factory functions — this is the only way to get them.
123
- *
124
- * @param fn - Template function that receives `{ name, context?, cause? }` and returns a message string
125
- */
126
- withMessage(fn: MessageFn<TName, TContext, TCause>): FinalFactories<TName, TContext, TCause>;
127
- };
128
- /**
129
- * Creates a new tagged error type with a fluent builder API.
130
- *
131
- * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.
132
- * `.withMessage(fn)` is **required** and **terminal** — it returns the factory functions.
133
- *
134
- * @example Simple error
135
- * ```ts
136
- * const { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')
137
- * .withMessage(() => 'A recording is already in progress');
138
- * ```
139
- *
140
- * @example Error with context
141
- * ```ts
142
- * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')
143
- * .withContext<{ table: string; id: string }>()
144
- * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);
145
- * ```
146
- *
147
- * @example Error with context and cause
148
- * ```ts
149
- * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')
150
- * .withContext<{ operation: string }>()
151
- * .withCause<DbServiceError>()
152
- * .withMessage(({ context, cause }) =>
153
- * `Operation '${context.operation}' failed: ${cause.message}`
154
- * );
155
- * ```
156
- */
157
- declare function createTaggedError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
92
+ //# sourceMappingURL=extractErrorMessage.d.ts.map
93
+
158
94
  //#endregion
159
- export { AnyTaggedError, JsonObject, JsonValue, TaggedError, createTaggedError, extractErrorMessage };
95
+ export { AnyTaggedError, DefineErrorsReturn, ErrorBody, ErrorsConfig, InferError, InferErrors, JsonObject, JsonValue, ValidatedConfig, defineErrors, extractErrorMessage };
160
96
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":"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"}
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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"}
@@ -1,6 +1,45 @@
1
1
  import { Err } from "../result-DnOm5ds5.js";
2
2
 
3
- //#region src/error/utils.ts
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,70 +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
- * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.
41
- * `.withMessage(fn)` is **required** and **terminal** — it returns the factory functions.
42
- *
43
- * @example Simple error
44
- * ```ts
45
- * const { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')
46
- * .withMessage(() => 'A recording is already in progress');
47
- * ```
48
- *
49
- * @example Error with context
50
- * ```ts
51
- * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')
52
- * .withContext<{ table: string; id: string }>()
53
- * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);
54
- * ```
55
- *
56
- * @example Error with context and cause
57
- * ```ts
58
- * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')
59
- * .withContext<{ operation: string }>()
60
- * .withCause<DbServiceError>()
61
- * .withMessage(({ context, cause }) =>
62
- * `Operation '${context.operation}' failed: ${cause.message}`
63
- * );
64
- * ```
65
- */
66
- function createTaggedError(name) {
67
- const createBuilder = () => {
68
- return {
69
- withContext() {
70
- return createBuilder();
71
- },
72
- withCause() {
73
- return createBuilder();
74
- },
75
- withMessage(fn) {
76
- const errorConstructor = (input) => {
77
- const messageInput = {
78
- name,
79
- ..."context" in input ? { context: input.context } : {},
80
- ..."cause" in input ? { cause: input.cause } : {}
81
- };
82
- return {
83
- name,
84
- message: input.message ?? fn(messageInput),
85
- ..."context" in input ? { context: input.context } : {},
86
- ..."cause" in input ? { cause: input.cause } : {}
87
- };
88
- };
89
- const errName = name.replace(/Error$/, "Err");
90
- const errConstructor = (input) => Err(errorConstructor(input));
91
- return {
92
- [name]: errorConstructor,
93
- [errName]: errConstructor
94
- };
95
- }
96
- };
97
- };
98
- return createBuilder();
99
- }
100
76
 
101
77
  //#endregion
102
- export { createTaggedError, extractErrorMessage };
78
+ export { defineErrors, extractErrorMessage };
103
79
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["error: unknown","name: TName","fn: MessageFn<TName, TContext, TCause>","input: ErrorCallInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type {\n\tTaggedError,\n\tAnyTaggedError,\n\tJsonObject,\n} from \"./types.js\";\nimport { Err } from \"../result/result.js\";\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\n/**\n * Replaces the \"Error\" suffix with \"Err\" suffix in error type names.\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Message Function Types\n// =============================================================================\n\n/**\n * Input provided to the message template function.\n * Contains everything the error will have except `message` (since that's what it computes).\n */\ntype MessageInput<\n\tTName extends string,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { name: TName } & ([TContext] extends [undefined]\n\t? Record<never, never>\n\t: { context: Exclude<TContext, undefined> }) &\n\t([TCause] extends [undefined]\n\t\t? Record<never, never>\n\t\t: { cause: Exclude<TCause, undefined> });\n\n/**\n * Message template function type.\n */\ntype MessageFn<\n\tTName extends string,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = (input: MessageInput<TName, TContext, TCause>) => string;\n\n// =============================================================================\n// Factory Input Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n */\ntype OptionalIfUndefined<T, TKey extends string> = undefined extends T\n\t? { [K in TKey]?: Exclude<T, undefined> }\n\t: { [K in TKey]: T };\n\n/**\n * Input type for error factory functions.\n * `message` is optional (computed from template when omitted).\n * `context` and `cause` follow the same optionality rules as before.\n */\ntype ErrorCallInput<\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message?: string } & (TContext extends undefined\n\t? Record<never, never>\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? Record<never, never>\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n// =============================================================================\n// Builder & Factory Types\n// =============================================================================\n\n/**\n * The final factories object returned by `.withMessage()`.\n * Has factory functions, NO chain methods.\n */\ntype FinalFactories<\n\tTName extends `${string}Error`,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorCallInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorCallInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent createTaggedError API.\n * Has chain methods only, NO factory functions.\n * Must call `.withMessage(fn)` to get factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends JsonObject | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = {\n\t/**\n\t * Constrains the context type for this error.\n\t * `.withContext<T>()` where T doesn't include undefined → context is **required**\n\t * `.withContext<T | undefined>()` → context is **optional** but typed when provided\n\t */\n\twithContext<\n\t\tT extends JsonObject | undefined = JsonObject | undefined,\n\t>(): ErrorBuilder<TName, T, TCause>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t * `.withCause<T>()` where T doesn't include undefined → cause is **required**\n\t * `.withCause<T | undefined>()` → cause is **optional** but typed when provided\n\t */\n\twithCause<\n\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t>(): ErrorBuilder<TName, TContext, T>;\n\n\t/**\n\t * Terminal method that defines how the error message is computed from its data.\n\t * Returns the factory functions — this is the only way to get them.\n\t *\n\t * @param fn - Template function that receives `{ name, context?, cause? }` and returns a message string\n\t */\n\twithMessage(\n\t\tfn: MessageFn<TName, TContext, TCause>,\n\t): FinalFactories<TName, TContext, TCause>;\n};\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.\n * `.withMessage(fn)` is **required** and **terminal** — it returns the factory functions.\n *\n * @example Simple error\n * ```ts\n * const { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')\n * .withMessage(() => 'A recording is already in progress');\n * ```\n *\n * @example Error with context\n * ```ts\n * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')\n * .withContext<{ table: string; id: string }>()\n * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);\n * ```\n *\n * @example Error with context and cause\n * ```ts\n * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')\n * .withContext<{ operation: string }>()\n * .withCause<DbServiceError>()\n * .withMessage(({ context, cause }) =>\n * `Operation '${context.operation}' failed: ${cause.message}`\n * );\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends JsonObject | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\treturn {\n\t\t\twithContext<\n\t\t\t\tT extends JsonObject | undefined = JsonObject | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<T, TCause>();\n\t\t\t},\n\t\t\twithCause<\n\t\t\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<TContext, T>();\n\t\t\t},\n\t\t\twithMessage(\n\t\t\t\tfn: MessageFn<TName, TContext, TCause>,\n\t\t\t): FinalFactories<TName, TContext, TCause> {\n\t\t\t\tconst errorConstructor = (\n\t\t\t\t\tinput: ErrorCallInput<TContext, TCause>,\n\t\t\t\t) => {\n\t\t\t\t\tconst messageInput = {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\t...(\"context\" in input ? { context: input.context } : {}),\n\t\t\t\t\t\t...(\"cause\" in input ? { cause: input.cause } : {}),\n\t\t\t\t\t} as MessageInput<TName, TContext, TCause>;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\tinput.message ?? fn(messageInput),\n\t\t\t\t\t\t...(\"context\" in input ? { context: input.context } : {}),\n\t\t\t\t\t\t...(\"cause\" in input ? { cause: input.cause } : {}),\n\t\t\t\t\t} as unknown as TaggedError<TName, TContext, TCause>;\n\t\t\t\t};\n\n\t\t\t\tconst errName = name.replace(\n\t\t\t\t\t/Error$/,\n\t\t\t\t\t\"Err\",\n\t\t\t\t) as ReplaceErrorWithErr<TName>;\n\t\t\t\tconst errConstructor = (\n\t\t\t\t\tinput: ErrorCallInput<TContext, TCause>,\n\t\t\t\t) => Err(errorConstructor(input));\n\n\t\t\t\treturn {\n\t\t\t\t\t[name]: errorConstructor,\n\t\t\t\t\t[errName]: errConstructor,\n\t\t\t\t} as FinalFactories<TName, TContext, TCause>;\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;AAaA,SAAgB,oBAAoBA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4JD,SAAgB,kBACfC,MACsB;CACtB,MAAM,gBAAgB,MAGwB;AAC7C,SAAO;GACN,cAEI;AACH,WAAO,eAA0B;GACjC;GACD,YAEI;AACH,WAAO,eAA4B;GACnC;GACD,YACCC,IAC0C;IAC1C,MAAM,mBAAmB,CACxBC,UACI;KACJ,MAAM,eAAe;MACpB;MACA,GAAI,aAAa,QAAQ,EAAE,SAAS,MAAM,QAAS,IAAG,CAAE;MACxD,GAAI,WAAW,QAAQ,EAAE,OAAO,MAAM,MAAO,IAAG,CAAE;KAClD;AAED,YAAO;MACN;MACA,SACC,MAAM,WAAW,GAAG,aAAa;MAClC,GAAI,aAAa,QAAQ,EAAE,SAAS,MAAM,QAAS,IAAG,CAAE;MACxD,GAAI,WAAW,QAAQ,EAAE,OAAO,MAAM,MAAO,IAAG,CAAE;KAClD;IACD;IAED,MAAM,UAAU,KAAK,QACpB,UACA,MACA;IACD,MAAM,iBAAiB,CACtBA,UACI,IAAI,iBAAiB,MAAM,CAAC;AAEjC,WAAO;MACL,OAAO;MACP,UAAU;IACX;GACD;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wellcrafted",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Delightful TypeScript patterns for elegant, type-safe applications",
5
5
  "type": "module",
6
6
  "files": [