@onrails/result 0.2.0 → 0.3.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.
Files changed (60) hide show
  1. package/DESIGN.md +10 -18
  2. package/README.md +14 -59
  3. package/RECIPES.md +12 -25
  4. package/dist/{async-u8U5P2EH.d.cts → async-BR3AX0ck.d.cts} +54 -18
  5. package/dist/{async-1fIhfzFu.d.ts → async-SZ7BEl0g.d.ts} +54 -18
  6. package/dist/compat/neverthrow.cjs +39 -51
  7. package/dist/compat/neverthrow.cjs.map +1 -1
  8. package/dist/compat/neverthrow.d.cts +3 -3
  9. package/dist/compat/neverthrow.d.ts +3 -3
  10. package/dist/compat/neverthrow.js +39 -51
  11. package/dist/compat/neverthrow.js.map +1 -1
  12. package/dist/extra.cjs +1 -1
  13. package/dist/extra.cjs.map +1 -1
  14. package/dist/extra.d.cts +5 -11
  15. package/dist/extra.d.ts +5 -11
  16. package/dist/extra.js +1 -1
  17. package/dist/extra.js.map +1 -1
  18. package/dist/fluent.cjs +55 -27
  19. package/dist/fluent.cjs.map +1 -1
  20. package/dist/fluent.d.cts +37 -6
  21. package/dist/fluent.d.ts +37 -6
  22. package/dist/fluent.js +55 -27
  23. package/dist/fluent.js.map +1 -1
  24. package/dist/index.cjs +80 -94
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +146 -44
  27. package/dist/index.d.ts +146 -44
  28. package/dist/index.js +77 -94
  29. package/dist/index.js.map +1 -1
  30. package/dist/railway.cjs +98 -172
  31. package/dist/railway.cjs.map +1 -1
  32. package/dist/railway.d.cts +25 -179
  33. package/dist/railway.d.ts +25 -179
  34. package/dist/railway.js +99 -162
  35. package/dist/railway.js.map +1 -1
  36. package/dist/try-gen.cjs +1 -1
  37. package/dist/try-gen.cjs.map +1 -1
  38. package/dist/try-gen.js +1 -1
  39. package/dist/try-gen.js.map +1 -1
  40. package/package.json +1 -16
  41. package/skills/railway-do-notation/SKILL.md +29 -68
  42. package/skills/result-composition/SKILL.md +2 -4
  43. package/dist/$.cjs +0 -40
  44. package/dist/$.cjs.map +0 -1
  45. package/dist/$.js +0 -36
  46. package/dist/$.js.map +0 -1
  47. package/dist/_.d.cts +0 -2
  48. package/dist/_.d.ts +0 -2
  49. package/dist/interop.cjs +0 -433
  50. package/dist/interop.cjs.map +0 -1
  51. package/dist/interop.d.cts +0 -75
  52. package/dist/interop.d.ts +0 -75
  53. package/dist/interop.js +0 -429
  54. package/dist/interop.js.map +0 -1
  55. package/dist/validation.cjs +0 -53
  56. package/dist/validation.cjs.map +0 -1
  57. package/dist/validation.d.cts +0 -72
  58. package/dist/validation.d.ts +0 -72
  59. package/dist/validation.js +0 -48
  60. package/dist/validation.js.map +0 -1
package/dist/railway.cjs CHANGED
@@ -1,5 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ // src/internal/dual.ts
4
+ var dual = (arity, body) => {
5
+ const run = body;
6
+ const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
7
+ return dispatcher;
8
+ };
9
+
3
10
  // src/result.ts
4
11
  var ok = (value) => ({
5
12
  _tag: "Ok",
@@ -11,18 +18,18 @@ var err = (error) => ({
11
18
  });
12
19
  var isOk = (result) => result._tag === "Ok";
13
20
  var isErr = (result) => result._tag === "Err";
14
- var mapImpl = (result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error);
15
- function map(...args) {
16
- if (args.length === 2) return mapImpl(args[0], args[1]);
17
- const fn = args[0];
18
- return (result) => mapImpl(result, fn);
19
- }
20
- var mapErrImpl = (result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value);
21
- function mapErr(...args) {
22
- if (args.length === 2) return mapErrImpl(args[0], args[1]);
23
- const fn = args[0];
24
- return (result) => mapErrImpl(result, fn);
25
- }
21
+ var map = dual(
22
+ 2,
23
+ (result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error)
24
+ );
25
+ var mapErr = dual(
26
+ 2,
27
+ (result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value)
28
+ );
29
+ var flatMap = dual(
30
+ 2,
31
+ (result, fn) => isOk(result) ? fn(result.value) : err(result.error)
32
+ );
26
33
  function trySync(fn, onThrow) {
27
34
  return (...args) => {
28
35
  try {
@@ -44,17 +51,6 @@ var UnexpectedError = class extends Error {
44
51
  };
45
52
 
46
53
  // src/async.ts
47
- var liftPromiseResult = async (promise, onDefect) => {
48
- try {
49
- const result = await promise;
50
- if (isErr(result)) {
51
- return result;
52
- }
53
- return isResultLike(result.value) ? result.value : result;
54
- } catch (error) {
55
- return err(onDefect(error));
56
- }
57
- };
58
54
  var sequenceSettled = (settled) => {
59
55
  const values = [];
60
56
  for (const result of settled) {
@@ -149,16 +145,23 @@ var ResultAsync = class _ResultAsync {
149
145
  }
150
146
  /**
151
147
  * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
152
- * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
153
- * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
148
+ * {@link ResultAsync}. A thrown defect is routed through `onDefect`,
149
+ * defaulting to {@link UnexpectedError}. The `Ok` value passes through
150
+ * verbatim — even when it happens to be Result-shaped.
154
151
  *
155
152
  * @param promise - a promise that already yields a `Result`
156
153
  * @param onDefect - maps an unexpected throw to the `Err` channel
157
- * @see {@link fromAsync} from `@onrails/result/interop`
154
+ * @see {@link fromAsync} from `@onrails/result`
158
155
  */
159
156
  static fromResultPromise(promise, onDefect) {
160
157
  const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
161
- return new _ResultAsync(() => liftPromiseResult(promise, mapDefect));
158
+ return new _ResultAsync(async () => {
159
+ try {
160
+ return await promise;
161
+ } catch (error) {
162
+ return err(mapDefect(error));
163
+ }
164
+ });
162
165
  }
163
166
  /**
164
167
  * Combines a homogeneous array of async results into one. Resolves
@@ -242,7 +245,7 @@ var ResultAsync = class _ResultAsync {
242
245
  *
243
246
  * @example
244
247
  * ```ts
245
- * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
248
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
246
249
  * ```
247
250
  */
248
251
  mapErr(fn) {
@@ -266,18 +269,12 @@ var ResultAsync = class _ResultAsync {
266
269
  return first;
267
270
  }
268
271
  const next = fn(first.value);
269
- if (next instanceof _ResultAsync) {
270
- return next.resolve();
271
- }
272
- if (isResultLike(next)) {
273
- return next;
274
- }
275
- return isCompatLike(next) ? next.inner : next;
272
+ return next instanceof _ResultAsync ? next.resolve() : next;
276
273
  });
277
274
  }
278
275
  /**
279
276
  * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
280
- * tier; prefer `flatMap` in new code.
277
+ * tier; prefer {@link flatMap} in new code.
281
278
  *
282
279
  * @example
283
280
  * ```ts
@@ -294,7 +291,7 @@ var ResultAsync = class _ResultAsync {
294
291
  *
295
292
  * @example
296
293
  * ```ts
297
- * loadFromCache(id).recover(() => loadFromOrigin(id));
294
+ * loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
298
295
  * ```
299
296
  */
300
297
  recover(fn) {
@@ -309,7 +306,7 @@ var ResultAsync = class _ResultAsync {
309
306
  }
310
307
  /**
311
308
  * neverthrow-compat alias of {@link recover}. Kept as the documented compat
312
- * tier; prefer `recover` in new code.
309
+ * tier; prefer {@link recover} in new code.
313
310
  *
314
311
  * @example
315
312
  * ```ts
@@ -417,16 +414,9 @@ var ResultAsync = class _ResultAsync {
417
414
  );
418
415
  }
419
416
  };
420
- function isResultLike(v) {
421
- return typeof v === "object" && v !== null && "_tag" in v && (v._tag === "Ok" || v._tag === "Err");
422
- }
423
- function isCompatLike(v) {
424
- return typeof v === "object" && v !== null && "inner" in v && isResultLike(v.inner);
425
- }
426
417
 
427
- // src/railway-core.ts
418
+ // src/railway.ts
428
419
  var addField = (ctx, key, value) => ({ ...ctx, [key]: value });
429
- var liftResult = (result) => ResultAsync.fromResult(result);
430
420
  var Railway = class _Railway {
431
421
  constructor(state) {
432
422
  this.state = state;
@@ -440,10 +430,7 @@ var Railway = class _Railway {
440
430
  static context(context) {
441
431
  return new _Railway({ mode: "sync", result: ok(context) });
442
432
  }
443
- /**
444
- * Start a sync workflow with a throwing function — `onThrow` maps any
445
- * exception to a typed error.
446
- */
433
+ /** Start a sync workflow with a throwing function. */
447
434
  static fromSync(key, fn, onThrow) {
448
435
  return _Railway.empty().fromSync(key, fn, onThrow);
449
436
  }
@@ -460,164 +447,103 @@ var Railway = class _Railway {
460
447
  return _Railway.empty().fromAsync(key, fn);
461
448
  }
462
449
  /**
463
- * Pure sync derivation — `fn` must not throw. Use {@link fromSync} for
464
- * throwing transforms.
450
+ * Rebuild the workflow in its current mode after transforming the carried
451
+ * result. Single re-link point for the state/phantom invariant: the mode
452
+ * tag is preserved verbatim, so `M` still describes the new state — TS
453
+ * cannot narrow the phantom `M` from the runtime tag, hence the cast.
465
454
  */
466
- derive(key, fn) {
467
- return this.fromResult(key, (ctx) => ok(fn(ctx)));
455
+ step(onSync, onAsync) {
456
+ const next = this.state.mode === "sync" ? { mode: "sync", result: onSync(this.state.result) } : { mode: "async", result: onAsync(this.state.result) };
457
+ return new _Railway(next);
468
458
  }
469
459
  /**
470
- * Throwing sync transform. Adds `{ [key]: T }` to context; converts any
471
- * exception to `Err<F>` via `onThrow`.
460
+ * Project the carried result into the mode-aware output type. Counterpart
461
+ * of {@link Railway.step} for terminal steps: the runtime branch matches
462
+ * the branch `RailwayOutput` picks for `M` (state/phantom invariant), but
463
+ * TS cannot resolve the conditional on an unresolved `M`, hence the cast.
472
464
  */
465
+ out(onSync, onAsync) {
466
+ return this.state.mode === "sync" ? onSync(this.state.result) : onAsync(this.state.result);
467
+ }
468
+ /** Lift the carried result into `ResultAsync` for async-upgrading steps. */
469
+ toAsync() {
470
+ return this.state.mode === "sync" ? ResultAsync.fromResult(this.state.result) : this.state.result;
471
+ }
472
+ /** Pure sync derivation. */
473
+ derive(key, fn) {
474
+ return this.fromResult(key, (ctx) => ok(fn(ctx)));
475
+ }
476
+ /** Throwing sync transform. */
473
477
  fromSync(key, fn, onThrow) {
474
478
  return this.fromResult(key, (ctx) => trySync(fn, onThrow)(ctx));
475
479
  }
476
- /**
477
- * Sync `Result`-returning step. Adds `{ [key]: T }` to context on `Ok`;
478
- * short-circuits the workflow on `Err`. Error union widens to `E | F`.
479
- */
480
+ /** Sync `Result`-returning step. */
480
481
  fromResult(key, fn) {
481
- if (this.state.mode === "sync") {
482
- const current = this.state.result;
483
- if (isErr(current)) {
484
- return new _Railway({ mode: "sync", result: err(current.error) });
485
- }
486
- const next = fn(current.value);
487
- return isErr(next) ? new _Railway({ mode: "sync", result: err(next.error) }) : new _Railway({
488
- mode: "sync",
489
- result: ok(addField(current.value, key, next.value))
490
- });
491
- }
492
- return new _Railway({
493
- mode: "async",
494
- result: this.state.result.flatMap(
482
+ return this.step(
483
+ (result) => flatMap(result, (ctx) => map(fn(ctx), (value) => addField(ctx, key, value))),
484
+ (result) => result.flatMap(
495
485
  (ctx) => ResultAsync.fromResult(fn(ctx)).map((value) => addField(ctx, key, value))
496
486
  )
497
- });
487
+ );
498
488
  }
499
- /**
500
- * Promise-returning step — upgrades the workflow to async mode. Reject
501
- * reasons go through `onReject` to become typed `Err<F>`.
502
- */
489
+ /** Promise-returning step — upgrades the workflow to async mode. */
503
490
  fromPromise(key, fn, onReject) {
504
491
  return this.fromAsync(key, (ctx) => ResultAsync.fromPromise(fn(ctx), onReject));
505
492
  }
506
- /**
507
- * `ResultAsync`-returning step — upgrades the workflow to async mode.
508
- * Already-typed error: no mapper needed.
509
- */
493
+ /** `ResultAsync`-returning step — upgrades the workflow to async mode. */
510
494
  fromAsync(key, fn) {
511
- const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
512
495
  return new _Railway({
513
496
  mode: "async",
514
- result: current.flatMap((ctx) => fn(ctx).map((value) => addField(ctx, key, value)))
497
+ result: this.toAsync().flatMap((ctx) => fn(ctx).map((value) => addField(ctx, key, value)))
515
498
  });
516
499
  }
517
- /**
518
- * Narrow a nullable context field into a required non-null field. If the
519
- * source field is `null` / `undefined`, the workflow short-circuits with
520
- * `onMissing(ctx)`.
521
- *
522
- * @example
523
- * ```ts
524
- * .fromPromise("row", ({ id }) => db.users.findFirst({ where: eq(users.id, id) }))
525
- * .require("user", "row", ({ id }) => ({ kind: "not_found" as const, id }))
526
- * // user is now User (non-null), not User | null
527
- * ```
528
- */
500
+ /** Narrow a nullable context field into a required non-null field. */
529
501
  require(key, source, onMissing) {
530
502
  return this.fromResult(key, (ctx) => {
531
503
  const value = ctx[source];
532
504
  return value == null ? err(onMissing(ctx)) : ok(value);
533
505
  });
534
506
  }
535
- /**
536
- * Run independent `ResultAsync` branches concurrently and merge their
537
- * named outputs back into context. Upgrades the workflow to async mode.
538
- * On multiple failures, the first `Err` in record-iteration order wins.
539
- *
540
- * @example
541
- * ```ts
542
- * .parallel({
543
- * recent: ({ userId }) => loadRecent(userId),
544
- * metrics: ({ userId }) => loadMetrics(userId),
545
- * })
546
- * // ctx now has { ..., recent, metrics }
547
- * ```
548
- */
507
+ /** Run independent `ResultAsync` branches concurrently and merge outputs. */
549
508
  parallel(branches) {
550
- const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
509
+ const merged = this.toAsync().flatMap(
510
+ (ctx) => ResultAsync.combineTupleParallel(
511
+ Object.entries(branches).map(([, branch]) => branch(ctx))
512
+ ).map(
513
+ (values) => (
514
+ // Branch outputs land under exactly R's keys; Object.fromEntries
515
+ // erases that correspondence, so reassert the merged shape.
516
+ {
517
+ ...ctx,
518
+ ...Object.fromEntries(Object.keys(branches).map((key, index) => [key, values[index]]))
519
+ }
520
+ )
521
+ )
522
+ );
551
523
  return new _Railway({
552
524
  mode: "async",
553
- result: current.flatMap(
554
- (ctx) => ResultAsync.combineTupleParallel(
555
- Object.entries(branches).map(([, branch]) => branch(ctx))
556
- ).map((values) => {
557
- const next = { ...ctx };
558
- Object.keys(branches).forEach((key, index) => {
559
- next[key] = values[index];
560
- });
561
- return next;
562
- })
563
- )
525
+ // BranchFn erases per-key types to `ResultAsync<unknown, unknown>`
526
+ // (ParallelOutput/ParallelError recover them from R), so re-assert the
527
+ // precise value/error union the branches actually produce.
528
+ result: merged
564
529
  });
565
530
  }
566
- /**
567
- * Project the final context into the workflow's output type. Hides the
568
- * internal context shape from callers.
569
- */
531
+ /** Project the final context into the workflow's output type. */
570
532
  select(fn) {
571
- if (this.state.mode === "sync") {
572
- const current = this.state.result;
573
- return isErr(current) ? err(current.error) : ok(fn(current.value));
574
- }
575
- return this.state.result.map(fn);
533
+ return this.out(
534
+ (result) => map(result, fn),
535
+ (result) => result.map(fn)
536
+ );
576
537
  }
577
- /**
578
- * Return the accumulated context as-is. Use when downstream code needs
579
- * every named field; prefer {@link select} when you can project to a DTO.
580
- */
538
+ /** Return the accumulated context as-is. */
581
539
  done() {
582
- return this.state.result;
540
+ return this.out(
541
+ (result) => result,
542
+ (result) => result
543
+ );
583
544
  }
584
545
  };
585
546
 
586
- // src/railway-steps.ts
587
- var parseWithParser = (parser, input) => typeof parser === "function" ? parser(input) : parser.parse(input);
588
- function railway(input, ...steps) {
589
- return steps.reduce(
590
- (current, step) => step(current),
591
- Railway.context({ input })
592
- );
593
- }
594
- var parseWith = (parser, onThrow) => ({
595
- as: (key) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow)
596
- });
597
- var parseNamed = (key, parser, onThrow) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow);
598
- var fromSyncNamed = (key, fn, onThrow) => (workflow) => workflow.fromSync(key, fn, onThrow);
599
- var fromResultNamed = (key, fn) => (workflow) => workflow.fromResult(key, fn);
600
- var fromPromiseNamed = (key, fn, onReject) => (workflow) => workflow.fromPromise(key, fn, onReject);
601
- var fromAsyncNamed = (key, fn) => (workflow) => workflow.fromAsync(key, fn);
602
- var deriveNamed = (key, fn) => (workflow) => workflow.derive(key, fn);
603
- var requireNamed = (key, source, onMissing) => (workflow) => workflow.require(key, source, onMissing);
604
- var parallelNamed = (branches) => (workflow) => {
605
- const typedBranches = branches;
606
- return workflow.parallel(typedBranches);
607
- };
608
- var select = (fn) => (workflow) => workflow.select(fn);
609
-
610
547
  exports.Railway = Railway;
611
- exports.deriveNamed = deriveNamed;
612
- exports.fromAsyncNamed = fromAsyncNamed;
613
- exports.fromPromiseNamed = fromPromiseNamed;
614
- exports.fromResultNamed = fromResultNamed;
615
- exports.fromSyncNamed = fromSyncNamed;
616
- exports.parallelNamed = parallelNamed;
617
- exports.parseNamed = parseNamed;
618
- exports.parseWith = parseWith;
619
- exports.railway = railway;
620
- exports.requireNamed = requireNamed;
621
- exports.select = select;
622
548
  //# sourceMappingURL=railway.cjs.map
623
549
  //# sourceMappingURL=railway.cjs.map