@barnum/barnum 0.2.3 → 0.4.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/artifacts/linux-arm64/barnum +0 -0
- package/artifacts/linux-x64/barnum +0 -0
- package/artifacts/macos-arm64/barnum +0 -0
- package/artifacts/macos-x64/barnum +0 -0
- package/artifacts/win-x64/barnum.exe +0 -0
- package/cli.cjs +33 -0
- package/dist/all.d.ts +43 -0
- package/dist/all.d.ts.map +1 -0
- package/dist/all.js +8 -0
- package/dist/ast.d.ts +476 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +419 -0
- package/dist/bind.d.ts +59 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +69 -0
- package/dist/builtins/array.d.ts +36 -0
- package/dist/builtins/array.d.ts.map +1 -0
- package/dist/builtins/array.js +93 -0
- package/dist/builtins/index.d.ts +6 -0
- package/dist/builtins/index.d.ts.map +1 -0
- package/dist/builtins/index.js +5 -0
- package/dist/builtins/scalar.d.ts +12 -0
- package/dist/builtins/scalar.d.ts.map +1 -0
- package/dist/builtins/scalar.js +41 -0
- package/dist/builtins/struct.d.ts +25 -0
- package/dist/builtins/struct.d.ts.map +1 -0
- package/dist/builtins/struct.js +67 -0
- package/dist/builtins/tagged-union.d.ts +54 -0
- package/dist/builtins/tagged-union.d.ts.map +1 -0
- package/dist/builtins/tagged-union.js +81 -0
- package/dist/builtins/with-resource.d.ts +23 -0
- package/dist/builtins/with-resource.d.ts.map +1 -0
- package/dist/builtins/with-resource.js +35 -0
- package/dist/chain.d.ts +3 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +8 -0
- package/dist/effect-id.d.ts +15 -0
- package/dist/effect-id.d.ts.map +1 -0
- package/dist/effect-id.js +16 -0
- package/dist/handler.d.ts +51 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +130 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/iterator.d.ts +32 -0
- package/dist/iterator.d.ts.map +1 -0
- package/dist/iterator.js +123 -0
- package/dist/option.d.ts +74 -0
- package/dist/option.d.ts.map +1 -0
- package/dist/option.js +141 -0
- package/dist/pipe.d.ts +12 -0
- package/dist/pipe.d.ts.map +1 -0
- package/dist/pipe.js +12 -0
- package/dist/race.d.ts +54 -0
- package/dist/race.d.ts.map +1 -0
- package/dist/race.js +116 -0
- package/dist/recursive.d.ts +40 -0
- package/dist/recursive.d.ts.map +1 -0
- package/dist/recursive.js +58 -0
- package/dist/result.d.ts +50 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +117 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +160 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +7 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +95 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +13 -0
- package/dist/try-catch.d.ts +24 -0
- package/dist/try-catch.d.ts.map +1 -0
- package/dist/try-catch.js +37 -0
- package/dist/values.d.ts +6 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +12 -0
- package/dist/worker.d.ts +15 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +58 -0
- package/package.json +42 -16
- package/src/all.ts +133 -0
- package/src/ast.ts +1301 -0
- package/src/bind.ts +162 -0
- package/src/builtins/array.ts +121 -0
- package/src/builtins/index.ts +17 -0
- package/src/builtins/scalar.ts +49 -0
- package/src/builtins/struct.ts +111 -0
- package/src/builtins/tagged-union.ts +142 -0
- package/src/builtins/with-resource.ts +69 -0
- package/src/chain.ts +17 -0
- package/src/effect-id.ts +30 -0
- package/src/handler.ts +263 -0
- package/src/index.ts +37 -0
- package/src/iterator.ts +243 -0
- package/src/option.ts +199 -0
- package/src/pipe.ts +138 -0
- package/src/race.ts +173 -0
- package/src/recursive.ts +129 -0
- package/src/result.ts +168 -0
- package/src/run.ts +209 -0
- package/src/runtime.ts +16 -0
- package/src/schema.ts +118 -0
- package/src/schemas.ts +21 -0
- package/src/try-catch.ts +57 -0
- package/src/values.ts +21 -0
- package/src/worker.ts +71 -0
- package/README.md +0 -19
- package/barnum-config-schema.json +0 -408
- package/cli.js +0 -20
- package/index.js +0 -23
package/src/bind.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Action,
|
|
3
|
+
type ExtractInput,
|
|
4
|
+
type ExtractOutput,
|
|
5
|
+
type TypedAction,
|
|
6
|
+
toAction,
|
|
7
|
+
typedAction,
|
|
8
|
+
} from "./ast.js";
|
|
9
|
+
import { chain } from "./chain.js";
|
|
10
|
+
import { all } from "./all.js";
|
|
11
|
+
import { identity, drop, getIndex } from "./builtins/index.js";
|
|
12
|
+
import { allocateResumeHandlerId, type ResumeHandlerId } from "./effect-id.js";
|
|
13
|
+
import { pipe } from "./pipe.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// VarRef — typed reference to a bound value
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A typed reference to a bound value. Output is `TValue`.
|
|
21
|
+
*
|
|
22
|
+
* Use `.then()` (not `pipe()`) when chaining a VarRef into a generic
|
|
23
|
+
* action like `pick` or `getField` — pipe overloads can't infer
|
|
24
|
+
* the generic's type parameter from the VarRef's output.
|
|
25
|
+
*/
|
|
26
|
+
export type VarRef<TValue> = TypedAction<any, TValue>;
|
|
27
|
+
|
|
28
|
+
function createVarRef<TValue>(
|
|
29
|
+
resumeHandlerId: ResumeHandlerId,
|
|
30
|
+
): VarRef<TValue> {
|
|
31
|
+
return typedAction({
|
|
32
|
+
kind: "ResumePerform",
|
|
33
|
+
resume_handler_id: resumeHandlerId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// InferVarRefs — map bindings to VarRef types
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps each binding's output type to a VarRef. TypeScript resolves
|
|
43
|
+
* ExtractOutput from each binding expression.
|
|
44
|
+
*
|
|
45
|
+
* Constraint is `Action[]` (not `Pipeable<any, any>[]`) so that
|
|
46
|
+
* `ExtractOutput` extracts the correct output type from the phantom
|
|
47
|
+
* fields on the concrete types without fighting invariant `__in` checks.
|
|
48
|
+
*/
|
|
49
|
+
export type InferVarRefs<TBindings extends Action[]> = {
|
|
50
|
+
[K in keyof TBindings]: VarRef<ExtractOutput<TBindings[K]>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// readVar — handler DAG for the nth binding
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns an action that extracts the nth value from the ResumeHandle's
|
|
59
|
+
* state tuple and passes state through unchanged. When a ResumePerform
|
|
60
|
+
* fires, the engine calls the handler with `[payload, state]`. For bind,
|
|
61
|
+
* `state` (index 1) is the full All output tuple. The handler produces
|
|
62
|
+
* `[state[n], state]` — value is state[n], new_state is state (unchanged).
|
|
63
|
+
*
|
|
64
|
+
* Expanded AST: All(Chain(GetIndex(1).unwrap(), GetIndex(n).unwrap()), GetIndex(1).unwrap())
|
|
65
|
+
*/
|
|
66
|
+
function readVar(n: number): Action {
|
|
67
|
+
return toAction(
|
|
68
|
+
all(
|
|
69
|
+
chain(toAction(getIndex(1).unwrap()), toAction(getIndex(n).unwrap())),
|
|
70
|
+
toAction(getIndex(1).unwrap()),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// bind — the user-facing function
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Bind concurrent values as VarRefs available throughout the body.
|
|
81
|
+
*
|
|
82
|
+
* All bindings are actions (Pipeable) evaluated concurrently with the
|
|
83
|
+
* pipeline input. The body callback receives an array of VarRefs,
|
|
84
|
+
* one per binding.
|
|
85
|
+
*
|
|
86
|
+
* Compiles to:
|
|
87
|
+
* Chain(
|
|
88
|
+
* All(...bindings, Identity),
|
|
89
|
+
* ResumeHandle(r0, readVar(0),
|
|
90
|
+
* ResumeHandle(r1, readVar(1),
|
|
91
|
+
* Chain(GetIndex(N), body)
|
|
92
|
+
* )
|
|
93
|
+
* )
|
|
94
|
+
* )
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Constraint for the body callback return type. Only requires the output
|
|
98
|
+
* phantom field — omits `__in` and `__in_co` so that body actions with
|
|
99
|
+
* any input type (e.g. pipelines starting from a VarRef) are assignable.
|
|
100
|
+
*/
|
|
101
|
+
type BodyResult<TOut> = Action & {
|
|
102
|
+
__out?: () => TOut;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function bind<TBindings extends Action[], TOut>(
|
|
106
|
+
bindings: [...TBindings],
|
|
107
|
+
body: (vars: InferVarRefs<TBindings>) => BodyResult<TOut>,
|
|
108
|
+
): TypedAction<ExtractInput<TBindings[number]>, TOut> {
|
|
109
|
+
// 1. Gensym one resumeHandlerId per binding.
|
|
110
|
+
const resumeHandlerIds = bindings.map(() => allocateResumeHandlerId());
|
|
111
|
+
|
|
112
|
+
// 2. Create VarRefs (ResumePerform nodes) for each binding.
|
|
113
|
+
const varRefs = resumeHandlerIds.map((id) => createVarRef(id));
|
|
114
|
+
|
|
115
|
+
// 3. Invoke the body callback with the VarRefs.
|
|
116
|
+
const bodyAction = toAction(body(varRefs as InferVarRefs<TBindings>));
|
|
117
|
+
|
|
118
|
+
// 4. Build nested Handles from inside out.
|
|
119
|
+
// Innermost: extract pipeline_input (last All element) → user body
|
|
120
|
+
const pipelineInputIndex = bindings.length;
|
|
121
|
+
let inner: Action = toAction(
|
|
122
|
+
chain(
|
|
123
|
+
toAction(getIndex(pipelineInputIndex).unwrap()),
|
|
124
|
+
toAction(bodyAction),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
for (let i = resumeHandlerIds.length - 1; i >= 0; i--) {
|
|
128
|
+
inner = {
|
|
129
|
+
kind: "ResumeHandle",
|
|
130
|
+
resume_handler_id: resumeHandlerIds[i],
|
|
131
|
+
handler: readVar(i),
|
|
132
|
+
body: inner,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 5. All(...bindings, identity()) → nested Handles
|
|
137
|
+
const allAction: Action = {
|
|
138
|
+
kind: "All",
|
|
139
|
+
actions: [...bindings.map((b) => toAction(b)), toAction(identity())],
|
|
140
|
+
};
|
|
141
|
+
return typedAction(toAction(chain(toAction(allAction), toAction(inner))));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// bindInput — bind the pipeline input
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convenience wrapper for the common pattern of capturing the pipeline
|
|
150
|
+
* input as a VarRef. The body's pipeline input is `never` — the input
|
|
151
|
+
* is dropped, so the body must access it through the VarRef.
|
|
152
|
+
*
|
|
153
|
+
* Sugar for: `bind([identity()], ([input]) => pipe(drop, body(input)))`
|
|
154
|
+
*
|
|
155
|
+
* TOut defaults to `any` so callers can specify just TIn:
|
|
156
|
+
* bindInput<FileEntry>((entry) => ...)
|
|
157
|
+
*/
|
|
158
|
+
export function bindInput<TIn, TOut = any>(
|
|
159
|
+
body: (input: VarRef<TIn>) => BodyResult<TOut>,
|
|
160
|
+
): TypedAction<TIn, TOut> {
|
|
161
|
+
return bind([identity()], ([input]) => pipe(drop, body(input)));
|
|
162
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Option as OptionT,
|
|
3
|
+
type TypedAction,
|
|
4
|
+
typedAction,
|
|
5
|
+
} from "../ast.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// GetIndex — extract a single element from an array by index
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export function getIndex<TTuple extends unknown[], TIndex extends number>(
|
|
12
|
+
index: TIndex,
|
|
13
|
+
): TypedAction<TTuple, OptionT<TTuple[TIndex]>> {
|
|
14
|
+
return typedAction({
|
|
15
|
+
kind: "Invoke",
|
|
16
|
+
handler: {
|
|
17
|
+
kind: "Builtin",
|
|
18
|
+
builtin: { kind: "GetIndex", index },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Flatten — flatten a nested array one level
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export function flatten<TElement>(): TypedAction<TElement[][], TElement[]> {
|
|
28
|
+
return typedAction({
|
|
29
|
+
kind: "Invoke",
|
|
30
|
+
handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// SplitFirst — head/tail decomposition of an array
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deconstruct an array into its first element and the remaining elements.
|
|
40
|
+
* `TElement[] → Option<[TElement, TElement[]]>`
|
|
41
|
+
*
|
|
42
|
+
* Returns `Some([first, rest])` for non-empty arrays, `None` for empty arrays.
|
|
43
|
+
* This is the array equivalent of cons/uncons — enables recursive iteration
|
|
44
|
+
* patterns via `loop` + `splitFirst` + `branch`.
|
|
45
|
+
*
|
|
46
|
+
* This is a builtin (SplitFirst) because it requires array-length branching
|
|
47
|
+
* that can't be composed from existing AST nodes.
|
|
48
|
+
*/
|
|
49
|
+
export function splitFirst<TElement>(): TypedAction<
|
|
50
|
+
TElement[],
|
|
51
|
+
OptionT<[TElement, TElement[]]>
|
|
52
|
+
> {
|
|
53
|
+
return typedAction({
|
|
54
|
+
kind: "Invoke",
|
|
55
|
+
handler: { kind: "Builtin", builtin: { kind: "SplitFirst" } },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// SplitLast — init/last decomposition of an array
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Deconstruct an array into the leading elements and the last element.
|
|
65
|
+
* `TElement[] → Option<[TElement[], TElement]>`
|
|
66
|
+
*
|
|
67
|
+
* Returns `Some([init, last])` for non-empty arrays, `None` for empty arrays.
|
|
68
|
+
* Mirror of `splitFirst` — enables processing from the tail end.
|
|
69
|
+
*
|
|
70
|
+
* This is a builtin (SplitLast) because it requires array-length branching
|
|
71
|
+
* that can't be composed from existing AST nodes.
|
|
72
|
+
*/
|
|
73
|
+
export function splitLast<TElement>(): TypedAction<
|
|
74
|
+
TElement[],
|
|
75
|
+
OptionT<[TElement[], TElement]>
|
|
76
|
+
> {
|
|
77
|
+
return typedAction({
|
|
78
|
+
kind: "Invoke",
|
|
79
|
+
handler: { kind: "Builtin", builtin: { kind: "SplitLast" } },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Slice — extract a sub-array from start to end
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Slice an array from `start` (inclusive) to `end` (exclusive).
|
|
89
|
+
* `T[] → T[]`
|
|
90
|
+
*
|
|
91
|
+
* Both indices are clamped to array length. If `end` is omitted, slices
|
|
92
|
+
* to the end of the array. Returns empty array if `start >= end`.
|
|
93
|
+
*/
|
|
94
|
+
export function slice<TElement>(
|
|
95
|
+
start: number,
|
|
96
|
+
end?: number,
|
|
97
|
+
): TypedAction<TElement[], TElement[]> {
|
|
98
|
+
const builtin: { kind: "Slice"; start: number; end?: number } =
|
|
99
|
+
end === undefined
|
|
100
|
+
? { kind: "Slice", start }
|
|
101
|
+
: { kind: "Slice", start, end };
|
|
102
|
+
return typedAction({
|
|
103
|
+
kind: "Invoke",
|
|
104
|
+
handler: { kind: "Builtin", builtin },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Range — produce an integer array [start, start+1, ..., end-1]
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export function range(start: number, end: number): TypedAction<any, number[]> {
|
|
113
|
+
const result: number[] = [];
|
|
114
|
+
for (let i = start; i < end; i++) {
|
|
115
|
+
result.push(i);
|
|
116
|
+
}
|
|
117
|
+
return typedAction({
|
|
118
|
+
kind: "Invoke",
|
|
119
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: result } },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { constant, identity, drop, panic } from "./scalar.js";
|
|
2
|
+
export { getField, wrapInField, merge, pick, allObject } from "./struct.js";
|
|
3
|
+
export {
|
|
4
|
+
getIndex,
|
|
5
|
+
flatten,
|
|
6
|
+
splitFirst,
|
|
7
|
+
splitLast,
|
|
8
|
+
slice,
|
|
9
|
+
range,
|
|
10
|
+
} from "./array.js";
|
|
11
|
+
export {
|
|
12
|
+
tag,
|
|
13
|
+
extractPrefix,
|
|
14
|
+
asOption,
|
|
15
|
+
taggedUnionSchema,
|
|
16
|
+
} from "./tagged-union.js";
|
|
17
|
+
export { withResource } from "./with-resource.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type TypedAction, typedAction } from "../ast.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Constant — produce a fixed value (takes no pipeline input)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export function constant<TValue>(value: TValue): TypedAction<any, TValue> {
|
|
8
|
+
return typedAction({
|
|
9
|
+
kind: "Invoke",
|
|
10
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value } },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Identity — pass input through unchanged
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function identity<TValue = any>(): TypedAction<TValue, TValue> {
|
|
19
|
+
return typedAction({
|
|
20
|
+
kind: "Invoke",
|
|
21
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Drop — discard pipeline value
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export const drop: TypedAction<any, void> = typedAction({
|
|
30
|
+
kind: "Invoke",
|
|
31
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Panic — halt execution with an error message
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Halt execution with a fatal error. Not caught by tryCatch.
|
|
40
|
+
* Analogous to Rust's `panic!`.
|
|
41
|
+
*
|
|
42
|
+
* Output type is `never` — a panic never produces a value.
|
|
43
|
+
*/
|
|
44
|
+
export function panic(message: string): TypedAction<any, never> {
|
|
45
|
+
return typedAction({
|
|
46
|
+
kind: "Invoke",
|
|
47
|
+
handler: { kind: "Builtin", builtin: { kind: "Panic", message } },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Action,
|
|
3
|
+
type ExtractOutput,
|
|
4
|
+
type MergeTuple,
|
|
5
|
+
type Pipeable,
|
|
6
|
+
type TypedAction,
|
|
7
|
+
toAction,
|
|
8
|
+
typedAction,
|
|
9
|
+
} from "../ast.js";
|
|
10
|
+
import { chain } from "../chain.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// GetField — extract a single field from an object
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export function getField<
|
|
17
|
+
TObj extends Record<string, unknown>,
|
|
18
|
+
TField extends keyof TObj & string,
|
|
19
|
+
>(field: TField): TypedAction<TObj, TObj[TField]> {
|
|
20
|
+
return typedAction({
|
|
21
|
+
kind: "Invoke",
|
|
22
|
+
handler: {
|
|
23
|
+
kind: "Builtin",
|
|
24
|
+
builtin: { kind: "GetField", field },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// WrapInField — wrap input as { <field>: <input> }
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export function wrapInField<TField extends string, TValue>(
|
|
34
|
+
field: TField,
|
|
35
|
+
): TypedAction<TValue, Record<TField, TValue>> {
|
|
36
|
+
return typedAction({
|
|
37
|
+
kind: "Invoke",
|
|
38
|
+
handler: {
|
|
39
|
+
kind: "Builtin",
|
|
40
|
+
builtin: { kind: "WrapInField", field },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Merge — merge a tuple of objects into a single object
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export function merge<TTuple extends Record<string, unknown>[]>(): TypedAction<
|
|
50
|
+
TTuple,
|
|
51
|
+
MergeTuple<TTuple>
|
|
52
|
+
> {
|
|
53
|
+
return typedAction({
|
|
54
|
+
kind: "Invoke",
|
|
55
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Pick — select named fields from an object
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export function pick<
|
|
64
|
+
TObj extends Record<string, unknown>,
|
|
65
|
+
TKeys extends (keyof TObj & string)[],
|
|
66
|
+
>(...keys: TKeys): TypedAction<TObj, Pick<TObj, TKeys[number]>> {
|
|
67
|
+
const actions = keys.map((key) =>
|
|
68
|
+
toAction(chain(toAction(getField(key)), toAction(wrapInField(key)))),
|
|
69
|
+
);
|
|
70
|
+
const allAction: Action = { kind: "All", actions };
|
|
71
|
+
return chain(toAction(allAction), toAction(merge())) as TypedAction<
|
|
72
|
+
TObj,
|
|
73
|
+
Pick<TObj, TKeys[number]>
|
|
74
|
+
>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// AllObject — run named actions concurrently, collect into an object
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run named actions concurrently on the same input, collecting results
|
|
83
|
+
* into an object with matching keys.
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* allObject({
|
|
87
|
+
* files: listFiles,
|
|
88
|
+
* config: loadConfig,
|
|
89
|
+
* })
|
|
90
|
+
* // TIn → { files: string[], config: Config }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Each action receives the pipeline input. Results are wrapped in
|
|
94
|
+
* `{ key: value }` via `wrapInField`, run concurrently via `All`,
|
|
95
|
+
* then merged into a single object.
|
|
96
|
+
*/
|
|
97
|
+
export function allObject<TActions extends Record<string, Pipeable<any, any>>>(
|
|
98
|
+
actions: TActions,
|
|
99
|
+
): TypedAction<
|
|
100
|
+
any,
|
|
101
|
+
{ [K in keyof TActions & string]: ExtractOutput<TActions[K]> }
|
|
102
|
+
> {
|
|
103
|
+
const wrapped = Object.entries(actions).map(([key, action]) =>
|
|
104
|
+
toAction(chain(action, wrapInField(key))),
|
|
105
|
+
);
|
|
106
|
+
const allAction: Action = { kind: "All", actions: wrapped };
|
|
107
|
+
return chain(toAction(allAction), toAction(merge())) as TypedAction<
|
|
108
|
+
any,
|
|
109
|
+
{ [K in keyof TActions & string]: ExtractOutput<TActions[K]> }
|
|
110
|
+
>;
|
|
111
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Option,
|
|
3
|
+
type TaggedUnion,
|
|
4
|
+
type TypedAction,
|
|
5
|
+
toAction,
|
|
6
|
+
typedAction,
|
|
7
|
+
} from "../ast.js";
|
|
8
|
+
import { chain } from "../chain.js";
|
|
9
|
+
import { all } from "../all.js";
|
|
10
|
+
import { constant } from "./scalar.js";
|
|
11
|
+
import { wrapInField, merge } from "./struct.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Tag — wrap input as a tagged union variant
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap input as a tagged union member. Requires the full variant map TDef
|
|
20
|
+
* so the output type carries __def for branch decomposition.
|
|
21
|
+
*
|
|
22
|
+
* Usage: tag<{ Ok: string; Err: number }, "Ok">("Ok")
|
|
23
|
+
* input: string → output: TaggedUnion<{ Ok: string; Err: number }>
|
|
24
|
+
*/
|
|
25
|
+
export function tag<
|
|
26
|
+
TEnumName extends string,
|
|
27
|
+
TDef extends Record<string, unknown>,
|
|
28
|
+
TKind extends keyof TDef & string,
|
|
29
|
+
>(
|
|
30
|
+
kind: TKind,
|
|
31
|
+
enumName: TEnumName,
|
|
32
|
+
): TypedAction<TDef[TKind], TaggedUnion<TEnumName, TDef>> {
|
|
33
|
+
const namespacedKind = `${enumName}.${kind}`;
|
|
34
|
+
return chain(
|
|
35
|
+
toAction(
|
|
36
|
+
all(
|
|
37
|
+
chain(
|
|
38
|
+
toAction(constant(namespacedKind)),
|
|
39
|
+
toAction(wrapInField("kind")),
|
|
40
|
+
),
|
|
41
|
+
wrapInField("value"),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
toAction(merge()),
|
|
45
|
+
) as TypedAction<TDef[TKind], TaggedUnion<TEnumName, TDef>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// ExtractPrefix — extract enum prefix from tagged value kind
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract the enum prefix from a tagged value's `kind` field.
|
|
54
|
+
*
|
|
55
|
+
* Input: `{ kind: "Result.Ok", value: 42 }`
|
|
56
|
+
* Output: `{ kind: "Result", value: { kind: "Result.Ok", value: 42 } }`
|
|
57
|
+
*
|
|
58
|
+
* If `kind` contains no `'.'`, the entire kind string becomes the prefix.
|
|
59
|
+
* Used internally by `branchFamily` for two-level dispatch.
|
|
60
|
+
*/
|
|
61
|
+
export function extractPrefix(): TypedAction {
|
|
62
|
+
return typedAction({
|
|
63
|
+
kind: "Invoke",
|
|
64
|
+
handler: { kind: "Builtin", builtin: { kind: "ExtractPrefix" } },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// AsOption — convert boolean to Option<void>
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert a boolean to `Option<void>`.
|
|
74
|
+
*
|
|
75
|
+
* `true` → `{ kind: "Option.Some", value: null }`
|
|
76
|
+
* `false` → `{ kind: "Option.None", value: null }`
|
|
77
|
+
*/
|
|
78
|
+
export function asOption(): TypedAction<boolean, Option<void>> {
|
|
79
|
+
return typedAction({
|
|
80
|
+
kind: "Invoke",
|
|
81
|
+
handler: { kind: "Builtin", builtin: { kind: "AsOption" } },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// TaggedUnion Zod schema constructor
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reverse of VoidToNull: maps `null` back to `void` in the def so that
|
|
91
|
+
* `taggedUnionSchema({ Clean: z.null() })` produces the same phantom __def
|
|
92
|
+
* as `TaggedUnion<{ Clean: void }>`.
|
|
93
|
+
*/
|
|
94
|
+
type NullToVoid<TDef> = {
|
|
95
|
+
[K in keyof TDef]: TDef[K] extends null ? void : TDef[K];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a Zod schema for a `TaggedUnion<TEnumName, TDef>` — a discriminated
|
|
100
|
+
* union of `{ kind: "EnumName.Variant"; value: V }` objects.
|
|
101
|
+
*
|
|
102
|
+
* Each key in `cases` becomes a variant with a namespaced kind string.
|
|
103
|
+
* Use `z.null()` for void variants.
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* const schema = taggedUnionSchema("ClassifyResult", {
|
|
107
|
+
* HasErrors: z.array(TypeErrorValidator),
|
|
108
|
+
* Clean: z.null(),
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function taggedUnionSchema<
|
|
113
|
+
TEnumName extends string,
|
|
114
|
+
TDef extends Record<string, z.ZodTypeAny>,
|
|
115
|
+
>(
|
|
116
|
+
enumName: TEnumName,
|
|
117
|
+
cases: TDef,
|
|
118
|
+
): z.ZodType<
|
|
119
|
+
TaggedUnion<
|
|
120
|
+
TEnumName,
|
|
121
|
+
NullToVoid<{ [K in keyof TDef & string]: z.infer<TDef[K]> }>
|
|
122
|
+
>
|
|
123
|
+
> {
|
|
124
|
+
type Out = TaggedUnion<
|
|
125
|
+
TEnumName,
|
|
126
|
+
NullToVoid<{ [K in keyof TDef & string]: z.infer<TDef[K]> }>
|
|
127
|
+
>;
|
|
128
|
+
const variants = Object.entries(cases).map(([kind, valueSchema]) =>
|
|
129
|
+
z.object({ kind: z.literal(`${enumName}.${kind}`), value: valueSchema }),
|
|
130
|
+
);
|
|
131
|
+
if (variants.length === 0) {
|
|
132
|
+
return z.never() as z.ZodType<Out>;
|
|
133
|
+
}
|
|
134
|
+
if (variants.length === 1) {
|
|
135
|
+
return variants[0] as z.ZodType<Out>;
|
|
136
|
+
}
|
|
137
|
+
return z.discriminatedUnion("kind", [
|
|
138
|
+
variants[0],
|
|
139
|
+
variants[1],
|
|
140
|
+
...variants.slice(2),
|
|
141
|
+
]) as z.ZodType<Out>;
|
|
142
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Pipeable, type TypedAction, toAction } from "../ast.js";
|
|
2
|
+
import { chain } from "../chain.js";
|
|
3
|
+
import { all } from "../all.js";
|
|
4
|
+
import { identity } from "./scalar.js";
|
|
5
|
+
import { merge } from "./struct.js";
|
|
6
|
+
import { getIndex } from "./array.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// WithResource — RAII-style create/action/dispose
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RAII-style resource management combinator.
|
|
14
|
+
*
|
|
15
|
+
* Runs `create` to acquire a resource, then merges the resource with the
|
|
16
|
+
* original input into a flat object (`TResource & TIn`) for the action.
|
|
17
|
+
* After the action completes, `dispose` receives the resource for cleanup.
|
|
18
|
+
* The overall combinator returns the action's output.
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* TIn → create → TResource
|
|
22
|
+
* → merge(TResource, TIn) → TResource & TIn
|
|
23
|
+
* → action(TResource & TIn) → TOut
|
|
24
|
+
* → dispose(TResource) → (discarded)
|
|
25
|
+
* → TOut
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function withResource<
|
|
29
|
+
TIn extends Record<string, unknown>,
|
|
30
|
+
TResource extends Record<string, unknown>,
|
|
31
|
+
TOut,
|
|
32
|
+
TDisposeOut = unknown,
|
|
33
|
+
>({
|
|
34
|
+
create,
|
|
35
|
+
action,
|
|
36
|
+
dispose,
|
|
37
|
+
}: {
|
|
38
|
+
create: Pipeable<TIn, TResource>;
|
|
39
|
+
action: Pipeable<TResource & TIn, TOut>;
|
|
40
|
+
dispose: Pipeable<TResource, TDisposeOut>;
|
|
41
|
+
}): TypedAction<TIn, TOut> {
|
|
42
|
+
// Step 1: all(create, identity) → [TResource, TIn] → merge → TResource & TIn
|
|
43
|
+
const acquireAndMerge = chain(
|
|
44
|
+
toAction(all(create, identity())),
|
|
45
|
+
toAction(merge()),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Step 2: all(action, identity) → [TOut, TResource & TIn]
|
|
49
|
+
const actionAndKeepMerged = all(toAction(action), toAction(identity()));
|
|
50
|
+
|
|
51
|
+
// Step 3: all(getIndex(0).unwrap(), chain(getIndex(1).unwrap(), dispose)) → [TOut, unknown]
|
|
52
|
+
const disposeAndKeepResult = all(
|
|
53
|
+
toAction(getIndex(0).unwrap()),
|
|
54
|
+
chain(toAction(getIndex(1).unwrap()), toAction(dispose)),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Step 4: getIndex(0).unwrap() → TOut
|
|
58
|
+
return chain(
|
|
59
|
+
toAction(
|
|
60
|
+
chain(
|
|
61
|
+
toAction(
|
|
62
|
+
chain(toAction(acquireAndMerge), toAction(actionAndKeepMerged)),
|
|
63
|
+
),
|
|
64
|
+
toAction(disposeAndKeepResult),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
toAction(getIndex(0).unwrap()),
|
|
68
|
+
) as TypedAction<TIn, TOut>;
|
|
69
|
+
}
|
package/src/chain.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Pipeable,
|
|
3
|
+
type TypedAction,
|
|
4
|
+
toAction,
|
|
5
|
+
typedAction,
|
|
6
|
+
} from "./ast.js";
|
|
7
|
+
|
|
8
|
+
export function chain<T1, T2, T3>(
|
|
9
|
+
first: Pipeable<T1, T2>,
|
|
10
|
+
rest: Pipeable<T2, T3>,
|
|
11
|
+
): TypedAction<T1, T3> {
|
|
12
|
+
return typedAction<T1, T3>({
|
|
13
|
+
kind: "Chain",
|
|
14
|
+
first: toAction(first),
|
|
15
|
+
rest: toAction(rest),
|
|
16
|
+
});
|
|
17
|
+
}
|