@onrails/result 0.1.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/DESIGN.md +119 -0
- package/LICENSE +21 -0
- package/README.md +323 -0
- package/RECIPES.md +367 -0
- package/dist/async-CCA1yK8q.d.cts +147 -0
- package/dist/async-DH_-dNIo.d.ts +147 -0
- package/dist/compat/neverthrow.cjs +446 -0
- package/dist/compat/neverthrow.cjs.map +1 -0
- package/dist/compat/neverthrow.d.cts +77 -0
- package/dist/compat/neverthrow.d.ts +77 -0
- package/dist/compat/neverthrow.js +435 -0
- package/dist/compat/neverthrow.js.map +1 -0
- package/dist/extra.cjs +37 -0
- package/dist/extra.cjs.map +1 -0
- package/dist/extra.d.cts +50 -0
- package/dist/extra.d.ts +50 -0
- package/dist/extra.js +31 -0
- package/dist/extra.js.map +1 -0
- package/dist/fluent.cjs +64 -0
- package/dist/fluent.cjs.map +1 -0
- package/dist/fluent.d.cts +28 -0
- package/dist/fluent.d.ts +28 -0
- package/dist/fluent.js +61 -0
- package/dist/fluent.js.map +1 -0
- package/dist/index.cjs +406 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +227 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.js +369 -0
- package/dist/index.js.map +1 -0
- package/dist/interop.cjs +248 -0
- package/dist/interop.cjs.map +1 -0
- package/dist/interop.d.cts +25 -0
- package/dist/interop.d.ts +25 -0
- package/dist/interop.js +244 -0
- package/dist/interop.js.map +1 -0
- package/dist/mcp.cjs +292 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +62 -0
- package/dist/mcp.d.ts +62 -0
- package/dist/mcp.js +284 -0
- package/dist/mcp.js.map +1 -0
- package/dist/pipe.cjs +16 -0
- package/dist/pipe.cjs.map +1 -0
- package/dist/pipe.d.cts +26 -0
- package/dist/pipe.d.ts +26 -0
- package/dist/pipe.js +14 -0
- package/dist/pipe.js.map +1 -0
- package/dist/railway.cjs +443 -0
- package/dist/railway.cjs.map +1 -0
- package/dist/railway.d.cts +214 -0
- package/dist/railway.d.ts +214 -0
- package/dist/railway.js +431 -0
- package/dist/railway.js.map +1 -0
- package/dist/try-gen.cjs +40 -0
- package/dist/try-gen.cjs.map +1 -0
- package/dist/try-gen.d.cts +23 -0
- package/dist/try-gen.d.ts +23 -0
- package/dist/try-gen.js +36 -0
- package/dist/try-gen.js.map +1 -0
- package/dist/types-C2Dp1d5J.d.cts +21 -0
- package/dist/types-C2Dp1d5J.d.ts +21 -0
- package/dist/validation.cjs +70 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +72 -0
- package/dist/validation.d.ts +72 -0
- package/dist/validation.js +65 -0
- package/dist/validation.js.map +1 -0
- package/package.json +114 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @onrails/result — design
|
|
2
|
+
|
|
3
|
+
Typed `Result` / `ResultAsync` for railway-oriented TypeScript. Tagged unions, tree-shakeable, neverthrow-compat shim available.
|
|
4
|
+
|
|
5
|
+
## Runtime model
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
type Result<T, E> =
|
|
9
|
+
| { readonly _tag: "Ok"; readonly value: T }
|
|
10
|
+
| { readonly _tag: "Err"; readonly error: E };
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Default API: dual-form module functions — every transform accepts either shape:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
map(result, fn); // data-first
|
|
17
|
+
map(fn)(result); // curried
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Dual functions: `map`, `mapErr`, `bimap`, `flatMap`, `recover`, `tap`, `tapErr`, `match`.
|
|
21
|
+
|
|
22
|
+
- `flatMap` is the canonical bind and widens error types (`E | F`).
|
|
23
|
+
- `match` is the canonical terminal collapse — positional, dual-form.
|
|
24
|
+
- `fold({ ok, err })(result)` is the curried named-slot escape valve when positional `match` order is ambiguous at the call site.
|
|
25
|
+
- `tap` / `tapErr` observe a track without changing the carried value.
|
|
26
|
+
- `recover` binds the error track and may return a failed workflow back to success.
|
|
27
|
+
- `pipe(value, ...fns)` is the variadic value-first pipe (up to 9 steps); `flow(...fns)` is the variadic point-free composition in `@onrails/result/pipe`.
|
|
28
|
+
- Optional dot-chaining via `fluent(r)` / `fluentAsync(ra)` in `@onrails/result/fluent`.
|
|
29
|
+
|
|
30
|
+
## Sync / async interop
|
|
31
|
+
|
|
32
|
+
Two distinct lift paths:
|
|
33
|
+
|
|
34
|
+
- `fromResult(result)` — already-known sync `Result<T, E>`. Cannot defect, so returns `ResultAsync<T, E>` without widening.
|
|
35
|
+
- `fromAsync(fn)` / `ResultAsync.fromResultPromise(promise)` — `Promise<Result<T, E>>` boundary. The promise can reject outside the `Result` channel, so the error widens with `UnexpectedError` unless a custom defect mapper is supplied.
|
|
36
|
+
|
|
37
|
+
`asyncAfter(result, fn)` bridges sync validation into async IO:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
asyncAfter(
|
|
41
|
+
trySync(() => Schema.parse(input), toError)(),
|
|
42
|
+
(value) => tryAsync(save(value)),
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`tryAsync(promise, onReject?)` wraps Promise boundaries with default `Error` normalization.
|
|
47
|
+
|
|
48
|
+
## Railway workflows
|
|
49
|
+
|
|
50
|
+
`Railway<C, E, M>` (`@onrails/result/railway`) is a named-context builder for service-layer ETL.
|
|
51
|
+
|
|
52
|
+
- `Railway.fromSync`, `.fromResult`, `.derive` preserve sync mode.
|
|
53
|
+
- `.fromPromise`, `.fromAsync`, `.parallel` upgrade to async mode.
|
|
54
|
+
- `.require(key, source, onMissing)` converts a nullable context field into a required non-null field.
|
|
55
|
+
- `.parallel(record)` runs independent `ResultAsync` branches concurrently and merges named outputs back into context. On multiple failures, the first `Err` in record iteration order wins.
|
|
56
|
+
- `.select(fn)` projects the final context.
|
|
57
|
+
|
|
58
|
+
Return type is mode-aware:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
type RailwayOutput<T, E, M extends RailwayMode> =
|
|
62
|
+
M extends "async" ? ResultAsync<T, E> : Result<T, E>;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `Railway` when named context removes nesting or positional tuple plumbing. Prefer `asyncAfter`, `fromResult`, or direct `flatMap` for one- or two-step flows.
|
|
66
|
+
|
|
67
|
+
`railway(input, ...steps)` is the functional companion for reusable workflow steps. Step factories: `parseWith(parser, onThrow).as(key)`, `fromSyncNamed`, `fromResultNamed`, `fromPromiseNamed`, `fromAsyncNamed`, `deriveNamed`, `requireNamed`, `parallelNamed`, `select`.
|
|
68
|
+
|
|
69
|
+
## Combining results
|
|
70
|
+
|
|
71
|
+
- `combine` / `combineTuple` — sync, first-Err wins.
|
|
72
|
+
- `sequenceTupleAsync` — async, sequential, first-Err in input order.
|
|
73
|
+
- `parallelTupleAsync` — async, concurrent (branches overlap), first-Err in input order.
|
|
74
|
+
- `ResultAsync.combine` — homogeneous async collection.
|
|
75
|
+
|
|
76
|
+
## Validation
|
|
77
|
+
|
|
78
|
+
`@onrails/result/validation` is a separate surface for accumulated independent failures (vs. railway short-circuit).
|
|
79
|
+
|
|
80
|
+
- `validateAll(results, join)` — combine errors via a join function.
|
|
81
|
+
- `validateAllArray(results)` — collect all errors into a readonly array.
|
|
82
|
+
- `validateTupleArray(results)` — same for heterogeneous tuple shapes.
|
|
83
|
+
|
|
84
|
+
Use `flatMap` for dependent checks where later checks need earlier successful values.
|
|
85
|
+
|
|
86
|
+
## Errors
|
|
87
|
+
|
|
88
|
+
- `E` is fully generic on the core package.
|
|
89
|
+
- `@onrails/result/extra` — `errOf`, `unionErrors`, `mapErrKind`, `declareErrors`, `AccumulateErrors` for discriminated unions.
|
|
90
|
+
|
|
91
|
+
## Exports
|
|
92
|
+
|
|
93
|
+
| Subpath | Purpose |
|
|
94
|
+
|---------|---------|
|
|
95
|
+
| `@onrails/result` | Core: dual-form map/flatMap/match, variadic `pipe`, sync collection, async surface, lift helpers, generator sugar |
|
|
96
|
+
| `@onrails/result/fluent` | Dot-style chains |
|
|
97
|
+
| `@onrails/result/extra` | Error-type utilities |
|
|
98
|
+
| `@onrails/result/interop` | Promise/ResultAsync boundary helpers |
|
|
99
|
+
| `@onrails/result/mcp` | MCP / HTTP boundary helpers |
|
|
100
|
+
| `@onrails/result/pipe` | Variadic `flow` for point-free composition |
|
|
101
|
+
| `@onrails/result/railway` | Named workflow builder and reusable step factories |
|
|
102
|
+
| `@onrails/result/try-gen` | Sync generator-style `Result` sugar |
|
|
103
|
+
| `@onrails/result/validation` | Independent validation with accumulated failures |
|
|
104
|
+
| `@onrails/result/compat/neverthrow` | Migration shim — class-shaped surface for incremental migration off neverthrow |
|
|
105
|
+
|
|
106
|
+
## Type tests
|
|
107
|
+
|
|
108
|
+
`test/types.spec.ts` — `ts-expect` (`expectType` + `TypeEqual`).
|
|
109
|
+
|
|
110
|
+
## v1.0 gate
|
|
111
|
+
|
|
112
|
+
- bun tests: core ops, FL functor/monad laws (sync), neverthrow-compat fixtures.
|
|
113
|
+
- README with tagged-error guidance.
|
|
114
|
+
- **Not** in v1.0: npm publish.
|
|
115
|
+
|
|
116
|
+
## Deferred
|
|
117
|
+
|
|
118
|
+
- npm publish visibility.
|
|
119
|
+
- `ap` / `alt` / error `Semigroup`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan R. Soares
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# @onrails/result
|
|
2
|
+
|
|
3
|
+
Tagged `Result` / `ResultAsync` for railway-oriented TypeScript. Pure tagged unions, neverthrow-shaped compat shim, FL-friendly.
|
|
4
|
+
|
|
5
|
+
## Install (local — pre-publish)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @onrails/result@file:../onrails/packages/result
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start (value-first — best inference)
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
asyncAfter,
|
|
16
|
+
err,
|
|
17
|
+
flatMapResult,
|
|
18
|
+
fromAsync,
|
|
19
|
+
mapResult,
|
|
20
|
+
match,
|
|
21
|
+
ok,
|
|
22
|
+
trySync,
|
|
23
|
+
} from "@onrails/result";
|
|
24
|
+
|
|
25
|
+
const parse = trySync(
|
|
26
|
+
(raw: string) => JSON.parse(raw),
|
|
27
|
+
(e) => ({ kind: "parse" as const, message: String(e) }),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const pipeline = flatMapResult(parse('{"v":1}'), (data) => ok(data.v));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Long chains: `fluent()` from `@onrails/result/fluent` or `flatMapResult` (not curried `flatMap`) for TS inference.
|
|
34
|
+
|
|
35
|
+
For worked examples of multi-step pipelines, parser builders, validator ladders, and parallel sub-workflows see [RECIPES.md](./RECIPES.md).
|
|
36
|
+
|
|
37
|
+
## When to use what
|
|
38
|
+
|
|
39
|
+
| Shape | Reach for |
|
|
40
|
+
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
41
|
+
| One or two sync steps | `flatMapResult`, `mapResult`, `match` |
|
|
42
|
+
| One or two async steps | `ResultAsync.flatMap`, `asyncAfter` |
|
|
43
|
+
| Long sync chain, value-first | `pipe(r, map(...), flatMap(...), ...)` |
|
|
44
|
+
| Long sync chain, dot-style preferred | `fluent(r)` from `@onrails/result/fluent` |
|
|
45
|
+
| Reusable composed function | `flow(...)` from `@onrails/result/pipe` |
|
|
46
|
+
| Several named sync/async steps | `Railway.*` (fluent) or `railway(...)` (functional, reusable steps) |
|
|
47
|
+
| Linear sync with early-return feel | `tryGen` + `$` from `@onrails/result/try-gen` |
|
|
48
|
+
| Independent validations, accumulated failures | `validateAll` / `validateTuple` from `@onrails/result/validation` |
|
|
49
|
+
| Sync → async lift, keep error type | `fromResult`, `asyncAfter` (do **not** use `fromAsync` here) |
|
|
50
|
+
| `Promise<Result<…>>` boundary lift | `fromAsync` / `tryAsync` |
|
|
51
|
+
|
|
52
|
+
Rule of thumb: pick the smallest tool that removes nesting. Reach for `Railway` only when named context replaces positional tuple plumbing.
|
|
53
|
+
|
|
54
|
+
## Sync → async boundaries
|
|
55
|
+
|
|
56
|
+
Use `fromResult` when a sync `Result` needs to enter a `ResultAsync` pipeline without widening the error channel:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { fromResult, ok, type Result } from "@onrails/result";
|
|
60
|
+
|
|
61
|
+
const parsed: Result<number, "parse"> = ok(1);
|
|
62
|
+
const asyncParsed = fromResult(parsed);
|
|
63
|
+
// ResultAsync<number, "parse"> — no UnexpectedError widening
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `asyncAfter` for the common "validate synchronously, then run async IO" shape:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { asyncAfter, tryAsync, trySync } from "@onrails/result";
|
|
70
|
+
|
|
71
|
+
return asyncAfter(
|
|
72
|
+
trySync(() => ArtifactSchema.parse(artifact), toError)(),
|
|
73
|
+
(validated) =>
|
|
74
|
+
tryAsync(
|
|
75
|
+
getDb()
|
|
76
|
+
.insert(artifacts)
|
|
77
|
+
.values(validated)
|
|
78
|
+
.then(() => undefined),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `tryAsync` for Promise boundaries with default `Error` normalization, or pass a custom rejection mapper:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const body = tryAsync(fetch(url).then((res) => res.text()));
|
|
87
|
+
|
|
88
|
+
const status = tryAsync(fetch(url), (error) => ({
|
|
89
|
+
kind: "network" as const,
|
|
90
|
+
message: String(error),
|
|
91
|
+
}));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Tagged error style
|
|
95
|
+
|
|
96
|
+
Prefer **tagged objects**, not bare `extends Error` classes — TS collapses structurally identical errors ([#652](https://github.com/supermacro/neverthrow/issues/652)).
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
type BotError =
|
|
100
|
+
| { kind: "not_found"; id: string }
|
|
101
|
+
| { kind: "network"; message: string };
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Helpers: `@onrails/result/extra` — `hasKind`, `mapErrKind`, `declareErrors`, `UnionErrors`, `AccumulateErrors`.
|
|
105
|
+
|
|
106
|
+
## Async interop — `fromAsync`
|
|
107
|
+
|
|
108
|
+
Lift `async` handlers that return `Result` without leaking `Promise<Result<…>>`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { fromAsync, ok, err } from "@onrails/result";
|
|
112
|
+
|
|
113
|
+
async function getItem(): Promise<Result<{ id: string }, HttpError>> {
|
|
114
|
+
if (!user) return err({ kind: "unauthorized" });
|
|
115
|
+
return ok({ id: "x" });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Public API: ResultAsync only
|
|
119
|
+
export const getItemAsync = fromAsync(getItem);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Awaitable `ResultAsync`
|
|
124
|
+
|
|
125
|
+
`ResultAsync` is thenable — `await ra` resolves to a bare tagged-union `Result<T, E>`. Narrow with `isOk(r)` / `isErr(r)` (type predicates) to read `.value` / `.error`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const r = await getItemAsync();
|
|
129
|
+
if (isOk(r)) console.log(r.value.id);
|
|
130
|
+
else console.error(r.error);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Match and unwrap helpers
|
|
134
|
+
|
|
135
|
+
`matchResult` is an alias for `match` for files that also import `match` from `ts-pattern`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { matchResult } from "@onrails/result";
|
|
139
|
+
import { match } from "ts-pattern";
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { unwrapOk } from "@onrails/result";
|
|
146
|
+
|
|
147
|
+
expect(unwrapOk(parseConfig(raw))).toEqual(expected);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## MCP / HTTP boundaries
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { toToolResponseAsync, unwrapFetchResultAsync } from "@onrails/result/mcp";
|
|
154
|
+
|
|
155
|
+
const ra = unwrapFetchResultAsync(
|
|
156
|
+
client.GET("/tokens/{id}"),
|
|
157
|
+
({ error, response }) => new PrintrApiError(response.status, detail),
|
|
158
|
+
);
|
|
159
|
+
return toToolResponseAsync(ra);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## `tryGen` — sync `?`
|
|
163
|
+
|
|
164
|
+
For short linear sync code:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { $, ok, tryGen } from "@onrails/result";
|
|
168
|
+
|
|
169
|
+
const out = tryGen(() => {
|
|
170
|
+
const a = $(parseA());
|
|
171
|
+
const b = $(parseB());
|
|
172
|
+
return ok(a + b);
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Use `sequenceTupleAsync` (or `parallelTupleAsync` when branches should overlap) when combining heterogeneous async results and destructuring the result:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { sequenceTupleAsync } from "@onrails/result";
|
|
180
|
+
|
|
181
|
+
const combined = sequenceTupleAsync([
|
|
182
|
+
loadSettings(),
|
|
183
|
+
loadModelCatalog(),
|
|
184
|
+
] as const);
|
|
185
|
+
|
|
186
|
+
const dto = combined.map(([settings, catalog]) =>
|
|
187
|
+
buildDto(settings, catalog),
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
When TS only infers the first error in a generator-style flow, use `declareErrors<E1 | E2>()` from `/extra`.
|
|
192
|
+
|
|
193
|
+
## `Railway` — named service workflows
|
|
194
|
+
|
|
195
|
+
Use `Railway` from `@onrails/result/railway` when a service workflow has several named sync/async steps and would otherwise need manual context-carrying objects:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { Railway } from "@onrails/result/railway";
|
|
199
|
+
|
|
200
|
+
const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), toError)
|
|
201
|
+
.fromPromise("row", ({ profileId }) => loadProfileRow(profileId), toError)
|
|
202
|
+
.require("profile", "row", ({ profileId }) => new Error(`Profile not found: ${profileId}`))
|
|
203
|
+
.derive("normalized", ({ profile }) => normalizeProfile(profile))
|
|
204
|
+
.fromResult("stats", ({ normalized }) => enrichProfileStats(normalized))
|
|
205
|
+
.parallel({
|
|
206
|
+
recentArtifacts: ({ normalized }) => loadRecentArtifacts(normalized.id),
|
|
207
|
+
jobMetrics: ({ normalized }) => loadJobMetrics(normalized.id),
|
|
208
|
+
})
|
|
209
|
+
.select(({ normalized, stats, recentArtifacts, jobMetrics }) =>
|
|
210
|
+
toProfileSummary({ normalized, stats, recentArtifacts, jobMetrics }),
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`, or `parallel` step upgrades the output to `ResultAsync<T, E>`.
|
|
215
|
+
|
|
216
|
+
Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMapResult`) for one or two steps where a builder would add ceremony.
|
|
217
|
+
|
|
218
|
+
## `railway(...)` — reusable workflow steps
|
|
219
|
+
|
|
220
|
+
Use lowercase `railway(...)` when the steps should be named once and reused across workflows:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import {
|
|
224
|
+
deriveNamed,
|
|
225
|
+
fromPromiseNamed,
|
|
226
|
+
parallelNamed,
|
|
227
|
+
parseWith,
|
|
228
|
+
railway,
|
|
229
|
+
requireNamed,
|
|
230
|
+
select,
|
|
231
|
+
} from "@onrails/result/railway";
|
|
232
|
+
|
|
233
|
+
const parseProfileId = parseWith(ProfileIdSchema, toError).as("profileId");
|
|
234
|
+
|
|
235
|
+
const loadProfileRow = fromPromiseNamed(
|
|
236
|
+
"row",
|
|
237
|
+
({ profileId }) => loadProfileRowById(profileId),
|
|
238
|
+
toError,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const requireProfile = requireNamed("profile", "row", ({ profileId }) =>
|
|
242
|
+
new Error(`Profile not found: ${profileId}`),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const loadSummaryInputs = parallelNamed({
|
|
246
|
+
recentArtifacts: ({ profile }) => loadRecentArtifacts(profile.id),
|
|
247
|
+
jobMetrics: ({ profile }) => loadJobMetrics(profile.id),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const summary = railway(
|
|
251
|
+
id,
|
|
252
|
+
parseProfileId,
|
|
253
|
+
loadProfileRow,
|
|
254
|
+
requireProfile,
|
|
255
|
+
deriveNamed("normalized", ({ profile }) => normalizeProfile(profile)),
|
|
256
|
+
loadSummaryInputs,
|
|
257
|
+
select(({ normalized, recentArtifacts, jobMetrics }) =>
|
|
258
|
+
toProfileSummary({ normalized, recentArtifacts, jobMetrics }),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`railway(input, ...steps)` starts from `{ input }`. `parseWith(...).as(key)` is the usual first step for raw input. The final output is still mode-aware: sync-only steps return `Result`, while async steps return `ResultAsync`.
|
|
264
|
+
|
|
265
|
+
## Pipe
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { pipe } from "@onrails/result";
|
|
269
|
+
import { flow } from "@onrails/result/pipe";
|
|
270
|
+
|
|
271
|
+
// Value-first variadic pipe — threads a starting value through unary steps.
|
|
272
|
+
const name = pipe(
|
|
273
|
+
parseConfig(raw),
|
|
274
|
+
map((cfg) => cfg.user),
|
|
275
|
+
flatMap((u) => (u.name ? ok(u.name) : err({ kind: "missing" }))),
|
|
276
|
+
recover((e) => (e.kind === "missing" ? ok("anon") : err(e))),
|
|
277
|
+
tap((n) => log(n)),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Variadic point-free composition — define a reusable pipeline.
|
|
281
|
+
const parseUserName = flow(
|
|
282
|
+
(raw: string) => parseConfig(raw),
|
|
283
|
+
map((cfg) => cfg.user),
|
|
284
|
+
flatMap((u) => (u.name ? ok(u.name) : err({ kind: "missing" }))),
|
|
285
|
+
);
|
|
286
|
+
parseUserName(raw);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## ESLint
|
|
290
|
+
|
|
291
|
+
`@onrails/eslint-plugin` — warns on `Promise<Result<…>>` and `_unsafeUnwrap*`.
|
|
292
|
+
|
|
293
|
+
## Migration from neverthrow
|
|
294
|
+
|
|
295
|
+
See [@onrails/codemod](../codemod/README.md) for the automated codemod, and the **Compat surface** notes below.
|
|
296
|
+
|
|
297
|
+
### Compat surface
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result/compat/neverthrow";
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- `Result` / `ResultAsync` are class-shaped (`CompatResult` / `CompatResultAsync`).
|
|
304
|
+
- `await ra` resolves to a `CompatResult<T, E>` (thenable), so `.isOk()`, `.value`, `.error`, `.match()`, `.unwrapOr()` all work without an extra `.resolve()` call.
|
|
305
|
+
- `andThen` / `chain` / `flatMap` / `orElse` accept any of `CompatResultAsync` / `ResultAsync` / `CompatResult` / tagged `Result` returns and union the error type.
|
|
306
|
+
- Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `flatMapResult`, `andThenResult`, `map`, `mapErr`, `orElse`, `match`, `unwrapOr`, `isOk`, `isErr`, `andTee`, `orTee`, `Result.combine`, `Result.fromThrowable`, `ResultAsync.combine`, `ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.fromThrowable`, `_unsafeUnwrap` / `_unsafeUnwrapErr`.
|
|
307
|
+
- Treat the compat surface as a migration step, not the destination — once a package migrates, switch its imports to `@onrails/result` and `@onrails/result/fluent`.
|
|
308
|
+
|
|
309
|
+
## Subpaths
|
|
310
|
+
|
|
311
|
+
| Path | Contents |
|
|
312
|
+
|------|----------|
|
|
313
|
+
| `@onrails/result` | Core + interop exports |
|
|
314
|
+
| `@onrails/result/fluent` | `fluent()`, `fluentAsync()` |
|
|
315
|
+
| `@onrails/result/extra` | Error-type utilities |
|
|
316
|
+
| `@onrails/result/interop` | `fromAsync`, `fromResult`, `asyncAfter` |
|
|
317
|
+
| `@onrails/result/mcp` | MCP / openapi-fetch helpers |
|
|
318
|
+
| `@onrails/result/pipe` | `flow` (variadic point-free composition) |
|
|
319
|
+
| `@onrails/result/railway` | `Railway`, `railway`, named workflow helpers |
|
|
320
|
+
| `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
|
|
321
|
+
| `@onrails/result/compat/neverthrow` | Migration shim |
|
|
322
|
+
|
|
323
|
+
See [DESIGN.md](./DESIGN.md).
|