funcade 1.0.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/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/funcade.d.ts +43 -0
- package/dist/funcade.js +29 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/methods.d.ts +16 -0
- package/dist/methods.js +10 -0
- package/dist/strategies.d.ts +54 -0
- package/dist/strategies.js +8 -0
- package/dist/types.d.ts +7 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxim Molochkov
|
|
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,287 @@
|
|
|
1
|
+
# Funcade
|
|
2
|
+
|
|
3
|
+
Build type-safe fluent object APIs from functional JavaScript libraries.
|
|
4
|
+
|
|
5
|
+
Funcade turns standalone functions into methods over a current value while preserving their
|
|
6
|
+
arguments, compatible overloads, and return types. It is immutable, has no runtime dependencies,
|
|
7
|
+
and does not modify the wrapped library.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install funcade
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
`funcade` accepts an initializer and a readonly list of method groups:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { add, format, sub, toDate } from 'date-fns'
|
|
21
|
+
import { chain, funcade, result } from 'funcade'
|
|
22
|
+
|
|
23
|
+
const date = funcade({
|
|
24
|
+
init: toDate,
|
|
25
|
+
methods: [chain({ add, sub }), result({ format })],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const formatted = date(new Date(2024, 0, 15))
|
|
29
|
+
.add({ days: 4 })
|
|
30
|
+
.sub({ days: 2 })
|
|
31
|
+
.format('yyyy-MM-dd')
|
|
32
|
+
|
|
33
|
+
// formatted: string
|
|
34
|
+
// => '2024-01-17'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `init` creates the initial current value and determines its type. Its parameters become the
|
|
38
|
+
parameters of the generated `date` factory.
|
|
39
|
+
- `methods` defines the methods shared by every wrapper created by that factory.
|
|
40
|
+
- Method overloads incompatible with the current value are removed from the generated API.
|
|
41
|
+
- A group is rejected if none of a method's overloads are compatible.
|
|
42
|
+
|
|
43
|
+
Calling the generated factory creates the first immutable wrapper. Chain methods create subsequent
|
|
44
|
+
wrappers, while result methods return a value directly.
|
|
45
|
+
|
|
46
|
+
## Method groups
|
|
47
|
+
|
|
48
|
+
Every function belongs to one of three method groups.
|
|
49
|
+
|
|
50
|
+
### `chain`
|
|
51
|
+
|
|
52
|
+
Calls the function and wraps its result in a new Funcade API. The original wrapper is not changed.
|
|
53
|
+
A chain method must return a value assignable to the current value type.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const numbers = funcade({
|
|
57
|
+
init: (value: number) => value,
|
|
58
|
+
methods: [
|
|
59
|
+
chain({
|
|
60
|
+
add: (value: number, amount: number) => value + amount,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const original = numbers(2)
|
|
66
|
+
const changed = original.add(3)
|
|
67
|
+
|
|
68
|
+
// original and changed are different wrappers
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `result`
|
|
72
|
+
|
|
73
|
+
Calls the function and returns its result directly, ending the chain. Result methods may return any
|
|
74
|
+
type.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const numbers = funcade({
|
|
78
|
+
init: (value: number) => value,
|
|
79
|
+
methods: [
|
|
80
|
+
result({
|
|
81
|
+
isPositive: (value: number) => value > 0,
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const positive = numbers(2).isPositive()
|
|
87
|
+
// positive: boolean
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `tap`
|
|
91
|
+
|
|
92
|
+
Calls the function for an observation or side effect and returns the same wrapper.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const numbers = funcade({
|
|
96
|
+
init: (value: number) => value,
|
|
97
|
+
methods: [
|
|
98
|
+
tap({
|
|
99
|
+
inspect: (value: number, label: string) => console.log(label, value),
|
|
100
|
+
}),
|
|
101
|
+
result({ read: (value: number) => value }),
|
|
102
|
+
],
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const value = numbers(2).inspect('current').read()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Method names must be unique across all groups in one Funcade API.
|
|
109
|
+
|
|
110
|
+
## Calling strategies
|
|
111
|
+
|
|
112
|
+
A strategy defines how a source function receives the current value and method arguments.
|
|
113
|
+
|
|
114
|
+
### Data-first
|
|
115
|
+
|
|
116
|
+
The default strategy expects the current value as the first argument:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
fn(value, ...methodArguments)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This is equivalent to passing `dataFirst` explicitly:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
chain({ add })
|
|
126
|
+
chain({ add }, dataFirst)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Curried FP functions
|
|
130
|
+
|
|
131
|
+
The `fp` strategy first applies the method arguments, then passes the current value to the returned
|
|
132
|
+
function:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
fn(...methodArguments)(value)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { add, format } from 'date-fns/fp'
|
|
140
|
+
import { chain, fp, funcade, result } from 'funcade'
|
|
141
|
+
|
|
142
|
+
const date = funcade({
|
|
143
|
+
init: (value: Date) => value,
|
|
144
|
+
methods: [chain({ add }, fp), result({ format }, fp)],
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
date(new Date()).add({ months: 1 }).format('yyyy-MM-dd')
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Custom strategies
|
|
151
|
+
|
|
152
|
+
Use `defineStrategy` when a library has a different application model. A custom strategy consists
|
|
153
|
+
of two independent parts:
|
|
154
|
+
|
|
155
|
+
- The function passed to `defineStrategy` implements the runtime call.
|
|
156
|
+
- A `StrategySpec` describes the same call to TypeScript. It does not exist at runtime.
|
|
157
|
+
|
|
158
|
+
### How a strategy specification works
|
|
159
|
+
|
|
160
|
+
`StrategySpec` acts as a type-level function. Funcade evaluates it separately for every source
|
|
161
|
+
function overload and provides two placeholders:
|
|
162
|
+
|
|
163
|
+
- `this['_fn']` is the source function overload currently being converted into a method.
|
|
164
|
+
- `this['_value']` is the value stored in the current Funcade wrapper.
|
|
165
|
+
|
|
166
|
+
The specification derives four public fields from those placeholders:
|
|
167
|
+
|
|
168
|
+
| Field | Meaning |
|
|
169
|
+
| -------- | ------------------------------------------------------------------------------------- |
|
|
170
|
+
| `args` | Arguments exposed by the generated object method. |
|
|
171
|
+
| `result` | Declared return type of the strategy. For `chain`, it must preserve the wrapped type. |
|
|
172
|
+
| `input` | Value type accepted by the source operation. |
|
|
173
|
+
| `value` | Relevant type represented by the current wrapped value. |
|
|
174
|
+
|
|
175
|
+
Funcade considers a method compatible when `value` is assignable to `input`. For a `chain` group,
|
|
176
|
+
it additionally checks that `result` is assignable to the type of `this['_value']`. This is why
|
|
177
|
+
`input` and `value` are separate: the wrapped value may be a container, such as a schema, while the
|
|
178
|
+
operation consumes the type represented by that container.
|
|
179
|
+
|
|
180
|
+
A specification has this general data flow:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
this['_fn'] -> args, input
|
|
184
|
+
this['_value'] -> value
|
|
185
|
+
runtime call -> result
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Do not assign `_fn` or `_value` in the specification. They are type-only placeholders supplied by
|
|
189
|
+
Funcade through `this`. The other fields should derive from them instead of referring to one
|
|
190
|
+
concrete function or wrapped value type. If a source function is overloaded, Funcade evaluates the
|
|
191
|
+
specification for each overload independently before assembling the generated method overloads.
|
|
192
|
+
|
|
193
|
+
The runtime callback intentionally accepts broad function, value, and argument types. TypeScript
|
|
194
|
+
cannot prove that its implementation matches the specification, so the specification is a contract
|
|
195
|
+
maintained by the strategy author. In particular, `result` must describe what the callback actually
|
|
196
|
+
returns, and the callback must interpret `args` in the same way as the `args` field.
|
|
197
|
+
|
|
198
|
+
### Valibot example
|
|
199
|
+
|
|
200
|
+
Valibot actions are not called with a schema. An action is created from the method arguments and
|
|
201
|
+
then composed with the current schema through `v.pipe`:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
action(...methodArguments) // creates an action
|
|
205
|
+
v.pipe(schema, action) // creates the next schema
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The corresponding specification needs to connect three different types: the action constructor,
|
|
209
|
+
the schema object, and the data flowing through that schema. Here is the complete strategy:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import * as v from 'valibot'
|
|
213
|
+
import { defineStrategy } from 'funcade'
|
|
214
|
+
import type { StrategySpec } from 'funcade'
|
|
215
|
+
|
|
216
|
+
type InputOf<T> = T extends {
|
|
217
|
+
readonly '~types'?: { readonly input: infer Input } | undefined
|
|
218
|
+
}
|
|
219
|
+
? Input
|
|
220
|
+
: unknown
|
|
221
|
+
|
|
222
|
+
type OutputOf<T> = T extends {
|
|
223
|
+
readonly '~types'?: { readonly output: infer Output } | undefined
|
|
224
|
+
}
|
|
225
|
+
? Output
|
|
226
|
+
: T
|
|
227
|
+
|
|
228
|
+
interface ValibotStrategySpec extends StrategySpec {
|
|
229
|
+
readonly args: Parameters<this['_fn']>
|
|
230
|
+
readonly result: this['_value']
|
|
231
|
+
readonly input: InputOf<ReturnType<this['_fn']>>
|
|
232
|
+
readonly value: OutputOf<this['_value']>
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const valibotStrategy = defineStrategy<ValibotStrategySpec>((action, schema, args) =>
|
|
236
|
+
v.pipe(schema, action(...args)),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Here is how each field maps to Valibot:
|
|
241
|
+
|
|
242
|
+
- `args` preserves all parameters of the action constructor. For example, `v.minLength` becomes
|
|
243
|
+
`.minLength(requirement, message?)`.
|
|
244
|
+
- `result` declares that `v.pipe` returns another schema suitable for the same wrapper API. Keeping
|
|
245
|
+
it as `this['_value']` allows the operation to be used in a `chain` group.
|
|
246
|
+
- `input` extracts the data type accepted by the created action.
|
|
247
|
+
- `value` extracts the output type represented by the current schema.
|
|
248
|
+
|
|
249
|
+
For a string schema and `v.minLength`, both compatibility types are `string`. With `v.minSize`, the
|
|
250
|
+
action input is `SizeInput`, which is incompatible with the schema's `string` output, so Funcade
|
|
251
|
+
rejects the method where the group is declared.
|
|
252
|
+
|
|
253
|
+
For one compatible `v.minLength` overload, Funcade evaluates the specification approximately as
|
|
254
|
+
follows:
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
_fn = (requirement: number, message?: ErrorMessage<string>) => MinLengthAction<string, ...>
|
|
258
|
+
_value = StringSchema<...>
|
|
259
|
+
|
|
260
|
+
args = [requirement: number, message?: ErrorMessage<string>]
|
|
261
|
+
result = StringSchema<...>
|
|
262
|
+
input = string
|
|
263
|
+
value = string
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The generated method therefore accepts the action constructor's arguments, while compatibility is
|
|
267
|
+
checked against the data flowing through the schema rather than against the schema object itself.
|
|
268
|
+
|
|
269
|
+
The complete API can then use the custom strategy like any built-in strategy:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import * as v from 'valibot'
|
|
273
|
+
import { chain, funcade, result } from 'funcade'
|
|
274
|
+
|
|
275
|
+
// valibotStrategy is defined above.
|
|
276
|
+
|
|
277
|
+
const stringSchema = funcade({
|
|
278
|
+
init: () => v.string(),
|
|
279
|
+
methods: [
|
|
280
|
+
chain({ trim: v.trim, minLength: v.minLength }, valibotStrategy),
|
|
281
|
+
result({ parse: v.parse, safeParse: v.safeParse }),
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
stringSchema().trim().minLength(3).parse(' hello ')
|
|
286
|
+
// => 'hello'
|
|
287
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BivariantFn, FnMap, IsAny } from "./types.js";
|
|
2
|
+
import { SpecFor, Strategy } from "./strategies.js";
|
|
3
|
+
import { MethodGroup, MethodGroupBase, MethodKind } from "./methods.js";
|
|
4
|
+
type TypeError<T extends readonly unknown[]> = {
|
|
5
|
+
readonly __typeError: T;
|
|
6
|
+
};
|
|
7
|
+
type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends ((value: infer U) => void) ? U : never;
|
|
8
|
+
type SpecArgs<Spec> = Spec extends {
|
|
9
|
+
readonly args: infer Args extends readonly unknown[];
|
|
10
|
+
} ? Args : never;
|
|
11
|
+
type SpecResult<Spec> = Spec extends {
|
|
12
|
+
readonly result: infer Result;
|
|
13
|
+
} ? Result : never;
|
|
14
|
+
type AnyArray = readonly unknown[];
|
|
15
|
+
type FunctionProperties<T extends object> = { [Key in keyof T]: T[Key]; };
|
|
16
|
+
/** @ts-expect-error TypeScript cannot verify extending the generic object type T. */
|
|
17
|
+
interface AddSignature<T extends object, Args extends AnyArray, Result> extends T {
|
|
18
|
+
(...args: Args): Result;
|
|
19
|
+
}
|
|
20
|
+
type Signature<Args extends AnyArray, Result> = (...args: Args) => Result;
|
|
21
|
+
type MethodResult<Kind extends MethodKind, Spec, Value, Groups extends readonly MethodGroup[]> = Kind extends "result" ? SpecResult<Spec> : FuncadeApi<Value, Groups>;
|
|
22
|
+
type IsAssignable<Value, Input> = IsAny<Value> extends true ? true : Value extends Input ? true : false;
|
|
23
|
+
type IsCompatible<Spec> = Spec extends {
|
|
24
|
+
readonly input: infer Input;
|
|
25
|
+
readonly value: infer Actual;
|
|
26
|
+
} ? IsAssignable<Actual, Input> : true;
|
|
27
|
+
type IsMethodCompatible<Spec, Kind extends MethodKind, Value> = IsCompatible<Spec> extends true ? Kind extends "chain" ? IsAssignable<SpecResult<Spec>, Value> : true : false;
|
|
28
|
+
type AddMethodSignature<Shape extends object, Args extends AnyArray, Result, S extends Strategy, Value, Kind extends MethodKind, Groups extends readonly MethodGroup[], Spec = SpecFor<Signature<Args, Result>, S, Value>> = IsMethodCompatible<Spec, Kind, Value> extends true ? AddSignature<Shape, SpecArgs<Spec>, MethodResult<Kind, Spec, Value, Groups>> : Shape;
|
|
29
|
+
type MapMethodOverloads<Fn extends object, S extends Strategy, Value, Kind extends MethodKind, Groups extends readonly MethodGroup[], Shape extends object, ResultShape extends object> = Shape extends Fn ? ResultShape : Fn extends AddSignature<Shape, infer Args, infer Result> ? MapMethodOverloads<Fn, S, Value, Kind, Groups, AddSignature<Shape, Args, Result>, AddMethodSignature<ResultShape, Args, Result, S, Value, Kind, Groups>> : ResultShape;
|
|
30
|
+
type MethodOverloads<Fn extends object, S extends Strategy, Value, Kind extends MethodKind, Groups extends readonly MethodGroup[]> = MapMethodOverloads<Fn, S, Value, Kind, Groups, FunctionProperties<Fn>, object>;
|
|
31
|
+
type GroupMethods<Value, Groups extends readonly MethodGroup[], Kind extends MethodKind, Methods extends FnMap, S extends Strategy> = { [Key in keyof Methods]: MethodOverloads<Methods[Key], S, Value, Kind, Groups>; };
|
|
32
|
+
type MethodsFromGroup<Value, Groups extends readonly MethodGroup[], Group> = Group extends MethodGroupBase<infer Kind, infer Methods, infer S> ? GroupMethods<Value, Groups, Kind, Methods, S> : unknown;
|
|
33
|
+
type FuncadeApi<Value, Groups extends readonly MethodGroup[]> = UnionToIntersection<MethodsFromGroup<Value, Groups, Groups[number]>>;
|
|
34
|
+
type HasCompatibleOverload<Fn extends object, S extends Strategy, Value, Kind extends MethodKind> = MethodOverloads<Fn, S, Value, Kind, readonly []> extends BivariantFn ? true : false;
|
|
35
|
+
type IncompatibleMethods<Value, Kind extends MethodKind, Methods extends FnMap, S extends Strategy> = { [Key in keyof Methods]: HasCompatibleOverload<Methods[Key], S, Value, Kind> extends true ? never : TypeError<[Key, "method is incompatible with the current value"]>; }[keyof Methods];
|
|
36
|
+
type IncompatibleGroup<Value, Group> = Group extends MethodGroupBase<infer Kind, infer Methods, infer S> ? IncompatibleMethods<Value, Kind, Methods, S> : never;
|
|
37
|
+
type ValidatedGroups<Value, Groups extends readonly MethodGroup[]> = { [Index in keyof Groups]: [IncompatibleGroup<Value, Groups[Index]>] extends [never] ? Groups[Index] : IncompatibleGroup<Value, Groups[Index]>; };
|
|
38
|
+
type FuncadeConfig<Init extends BivariantFn, Groups extends readonly MethodGroup[]> = {
|
|
39
|
+
init: Init;
|
|
40
|
+
methods: Groups & ValidatedGroups<ReturnType<NoInfer<Init>>, NoInfer<Groups>>;
|
|
41
|
+
};
|
|
42
|
+
declare function funcade<const Init extends BivariantFn, const Groups extends readonly MethodGroup[]>(config: FuncadeConfig<Init, Groups>): (...args: Parameters<Init>) => FuncadeApi<ReturnType<Init>, Groups>;
|
|
43
|
+
export { FuncadeApi, FuncadeConfig, TypeError, funcade };
|
package/dist/funcade.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const apiValue = Symbol("funcade.apiValue");
|
|
2
|
+
function funcade(config) {
|
|
3
|
+
const prototype = Object.create(null);
|
|
4
|
+
const createApi = (value) => {
|
|
5
|
+
const api = Object.create(prototype);
|
|
6
|
+
api[apiValue] = value;
|
|
7
|
+
return api;
|
|
8
|
+
};
|
|
9
|
+
for (const group of config.methods) {
|
|
10
|
+
const { kind, strategy } = group;
|
|
11
|
+
for (const [name, fn] of Object.entries(group.methods)) {
|
|
12
|
+
if (Object.hasOwn(prototype, name)) throw new Error(`Duplicate objectified API method: "${name}"`);
|
|
13
|
+
let method;
|
|
14
|
+
if (kind === "result") method = function(...args) {
|
|
15
|
+
return strategy(fn, this[apiValue], args);
|
|
16
|
+
};
|
|
17
|
+
else if (kind === "tap") method = function(...args) {
|
|
18
|
+
strategy(fn, this[apiValue], args);
|
|
19
|
+
return this;
|
|
20
|
+
};
|
|
21
|
+
else method = function(...args) {
|
|
22
|
+
return createApi(strategy(fn, this[apiValue], args));
|
|
23
|
+
};
|
|
24
|
+
prototype[name] = method;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return (...args) => createApi(config.init(...args));
|
|
28
|
+
}
|
|
29
|
+
export { funcade };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DataFirstStrategy, FpStrategy, Strategy, StrategyApply, StrategySpec, dataFirst, defineStrategy, fp } from "./strategies.js";
|
|
2
|
+
import { ChainGroup, MethodGroup, MethodGroupBase, MethodKind, ResultGroup, TapGroup, chain, result, tap } from "./methods.js";
|
|
3
|
+
import { FuncadeApi, FuncadeConfig, TypeError, funcade } from "./funcade.js";
|
|
4
|
+
export { type ChainGroup, type DataFirstStrategy, type FpStrategy, type FuncadeApi, type FuncadeConfig, type MethodGroup, type MethodGroupBase, type MethodKind, type ResultGroup, type Strategy, type StrategyApply, type StrategySpec, type TapGroup, type TypeError, chain, dataFirst, defineStrategy, fp, funcade, result, tap };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FnMap } from "./types.js";
|
|
2
|
+
import { DataFirstStrategy, Strategy } from "./strategies.js";
|
|
3
|
+
type MethodKind = "chain" | "result" | "tap";
|
|
4
|
+
type MethodGroupBase<Kind extends MethodKind, Methods extends FnMap = FnMap, S extends Strategy = Strategy> = Readonly<{
|
|
5
|
+
kind: Kind;
|
|
6
|
+
strategy: S;
|
|
7
|
+
methods: Methods;
|
|
8
|
+
}>;
|
|
9
|
+
type ChainGroup<Methods extends FnMap = FnMap, S extends Strategy = Strategy> = MethodGroupBase<"chain", Methods, S>;
|
|
10
|
+
type ResultGroup<Methods extends FnMap = FnMap, S extends Strategy = Strategy> = MethodGroupBase<"result", Methods, S>;
|
|
11
|
+
type TapGroup<Methods extends FnMap = FnMap, S extends Strategy = Strategy> = MethodGroupBase<"tap", Methods, S>;
|
|
12
|
+
type MethodGroup = MethodGroupBase<MethodKind>;
|
|
13
|
+
declare const chain: <const Methods extends FnMap, S extends Strategy = DataFirstStrategy>(methods: Methods, strategy?: S) => ChainGroup<Methods, NoInfer<S>>;
|
|
14
|
+
declare const result: <const Methods extends FnMap, S extends Strategy = DataFirstStrategy>(methods: Methods, strategy?: S) => ResultGroup<Methods, NoInfer<S>>;
|
|
15
|
+
declare const tap: <const Methods extends FnMap, S extends Strategy = DataFirstStrategy>(methods: Methods, strategy?: S) => TapGroup<Methods, NoInfer<S>>;
|
|
16
|
+
export { ChainGroup, MethodGroup, MethodGroupBase, MethodKind, ResultGroup, TapGroup, chain, result, tap };
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dataFirst } from "./strategies.js";
|
|
2
|
+
const createGroup = (kind, methods, strategy) => ({
|
|
3
|
+
kind,
|
|
4
|
+
strategy: strategy ?? dataFirst,
|
|
5
|
+
methods
|
|
6
|
+
});
|
|
7
|
+
const chain = (methods, strategy) => createGroup("chain", methods, strategy);
|
|
8
|
+
const result = (methods, strategy) => createGroup("result", methods, strategy);
|
|
9
|
+
const tap = (methods, strategy) => createGroup("tap", methods, strategy);
|
|
10
|
+
export { chain, result, tap };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AnyFn, BivariantFn, IsAny } from "./types.js";
|
|
2
|
+
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest] ? Rest : [];
|
|
3
|
+
type StrategyApply = (fn: AnyFn, value: any, args: readonly unknown[]) => unknown;
|
|
4
|
+
type StrategySpec = Readonly<{
|
|
5
|
+
_fn: BivariantFn;
|
|
6
|
+
_value: unknown;
|
|
7
|
+
args: unknown[];
|
|
8
|
+
result: unknown;
|
|
9
|
+
input: unknown;
|
|
10
|
+
value: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
type FnOf<T> = T extends BivariantFn ? T : never;
|
|
13
|
+
type ApplyStrategySpec<GetSpec extends StrategySpec, Fn, Value> = {
|
|
14
|
+
args: (GetSpec & {
|
|
15
|
+
readonly _fn: Fn;
|
|
16
|
+
readonly _value: Value;
|
|
17
|
+
})["args"];
|
|
18
|
+
result: (GetSpec & {
|
|
19
|
+
readonly _fn: Fn;
|
|
20
|
+
readonly _value: Value;
|
|
21
|
+
})["result"];
|
|
22
|
+
input: (GetSpec & {
|
|
23
|
+
readonly _fn: Fn;
|
|
24
|
+
})["input"];
|
|
25
|
+
value: (GetSpec & {
|
|
26
|
+
readonly _value: Value;
|
|
27
|
+
})["value"];
|
|
28
|
+
};
|
|
29
|
+
type Strategy<GetSpec extends StrategySpec = StrategySpec> = StrategyApply & {
|
|
30
|
+
readonly __spec: GetSpec;
|
|
31
|
+
};
|
|
32
|
+
interface DataFirstSpec extends StrategySpec {
|
|
33
|
+
readonly args: Tail<Parameters<FnOf<this["_fn"]>>>;
|
|
34
|
+
readonly result: ReturnType<FnOf<this["_fn"]>>;
|
|
35
|
+
readonly input: Parameters<FnOf<this["_fn"]>>[0];
|
|
36
|
+
readonly value: this["_value"];
|
|
37
|
+
}
|
|
38
|
+
interface FpSpec extends StrategySpec {
|
|
39
|
+
readonly args: CurriedApplyArgs<FnOf<this["_fn"]>>;
|
|
40
|
+
readonly result: CurriedApplyResult<FnOf<this["_fn"]>>;
|
|
41
|
+
readonly input: CurriedValue<FnOf<this["_fn"]>>;
|
|
42
|
+
readonly value: this["_value"];
|
|
43
|
+
}
|
|
44
|
+
type DataFirstStrategy = Strategy<DataFirstSpec>;
|
|
45
|
+
type FpStrategy = Strategy<FpSpec>;
|
|
46
|
+
type SpecFor<Fn, S extends Strategy, Value> = ApplyStrategySpec<S["__spec"], Fn, Value>;
|
|
47
|
+
type CurriedConsumer<Fn extends BivariantFn> = IsAny<ReturnType<Fn>> extends true ? never : ReturnType<Fn> extends BivariantFn ? ReturnType<Fn> : never;
|
|
48
|
+
type CurriedApplyArgs<Fn extends BivariantFn> = CurriedConsumer<Fn> extends never ? never : Parameters<Fn>;
|
|
49
|
+
type CurriedApplyResult<Fn extends BivariantFn> = ReturnType<CurriedConsumer<Fn>>;
|
|
50
|
+
type CurriedValue<Fn extends BivariantFn> = Parameters<CurriedConsumer<Fn>>[0];
|
|
51
|
+
declare const defineStrategy: <GetSpec extends StrategySpec = StrategySpec>(apply: StrategyApply) => Strategy<GetSpec>;
|
|
52
|
+
declare const dataFirst: Strategy<DataFirstSpec>;
|
|
53
|
+
declare const fp: Strategy<FpSpec>;
|
|
54
|
+
export { DataFirstStrategy, FpStrategy, SpecFor, Strategy, StrategyApply, StrategySpec, dataFirst, defineStrategy, fp };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const defineStrategy = (apply) => apply;
|
|
2
|
+
const dataFirst = defineStrategy((fn, value, args) => fn(value, ...args));
|
|
3
|
+
const fp = defineStrategy((fn, value, args) => {
|
|
4
|
+
const partiallyApplied = fn(...args);
|
|
5
|
+
if (typeof partiallyApplied !== "function") throw new TypeError("FP strategy expects a function that returns another function before receiving the current value");
|
|
6
|
+
return partiallyApplied(value);
|
|
7
|
+
});
|
|
8
|
+
export { dataFirst, defineStrategy, fp };
|
package/dist/types.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "funcade",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Build type-safe fluent object APIs from functional JavaScript libraries",
|
|
6
|
+
"author": "Maxim Molochkov <klaseca@gmail.com> (https://github.com/klaseca)",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"functional-programming",
|
|
9
|
+
"fp",
|
|
10
|
+
"fluent-api",
|
|
11
|
+
"method-chaining",
|
|
12
|
+
"api-adapter",
|
|
13
|
+
"facade",
|
|
14
|
+
"currying",
|
|
15
|
+
"immutable",
|
|
16
|
+
"wrapper"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/klaseca/funcade#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/klaseca/funcade.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/klaseca/funcade/issues"
|
|
26
|
+
},
|
|
27
|
+
"devEngines": {
|
|
28
|
+
"runtime": {
|
|
29
|
+
"name": "node",
|
|
30
|
+
"version": ">=24.18"
|
|
31
|
+
},
|
|
32
|
+
"packageManager": {
|
|
33
|
+
"name": "npm",
|
|
34
|
+
"version": ">=11"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"exports": {
|
|
38
|
+
".": "./dist/index.js",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepare": "husky",
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"dev": "tsdown --watch",
|
|
48
|
+
"test": "node --test",
|
|
49
|
+
"test:watch": "node --test --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"fmt": "prettier -w -u .",
|
|
52
|
+
"prerelease": "npm run typecheck && npm run test",
|
|
53
|
+
"release": "bumpp",
|
|
54
|
+
"prepublishOnly": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^26.1.1",
|
|
58
|
+
"bumpp": "^11.1.0",
|
|
59
|
+
"date-fns": "^4.1.0",
|
|
60
|
+
"expect-type": "^1.4.0",
|
|
61
|
+
"husky": "^9.1.7",
|
|
62
|
+
"lint-staged": "^17.0.8",
|
|
63
|
+
"prettier": "^3.9.5",
|
|
64
|
+
"tsdown": "^0.22.9",
|
|
65
|
+
"typescript": "^7.0.2",
|
|
66
|
+
"valibot": "^1.4.2"
|
|
67
|
+
}
|
|
68
|
+
}
|