feat: more mixins
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
export interface HKT<In = unknown, Out = unknown> {
|
export interface HKT<In = unknown, Out = unknown> {
|
||||||
readonly _meta: In;
|
readonly _meta: In;
|
||||||
readonly _t: In;
|
readonly _t: unknown;
|
||||||
new: (t: never) => Out;
|
new: (t: never) => Out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ type Param<
|
|||||||
export namespace HKT {
|
export namespace HKT {
|
||||||
export type T<
|
export type T<
|
||||||
This extends HKT,
|
This extends HKT,
|
||||||
T = This extends { _t: infer I } ? I : unknown,
|
T = This extends { _meta: infer I } ? I : unknown,
|
||||||
> = Param<This, T>;
|
> = Param<This, T>;
|
||||||
export type Apply<T extends HKT, t extends T["_t"]> = ReturnType<
|
export type Apply<T extends HKT, t extends T["_t"]> = ReturnType<
|
||||||
(T & { _t: t })["new"]
|
(T & { _t: t })["new"]
|
||||||
|
|||||||
+7
-4
@@ -1,18 +1,21 @@
|
|||||||
import type { DefaultRegistry, Methods, Registry } from "../registry";
|
import type { DefaultRegistry, Methods, Registry } from "../registry";
|
||||||
|
import type { shim } from "./mixin";
|
||||||
|
|
||||||
export interface Base<T> {
|
export type Base<T> = T extends { [shim]: { value: infer U } }
|
||||||
|
? { readonly value: U }
|
||||||
|
: {
|
||||||
readonly value: T;
|
readonly value: T;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Fluent<
|
export type Fluent<
|
||||||
T,
|
T,
|
||||||
Reg extends Registry = DefaultRegistry,
|
Reg extends Registry = DefaultRegistry,
|
||||||
> = Base<T> & Methods<T, Reg> & { readonly __registry: Reg };
|
> = Base<T> & Methods<T, Reg>;
|
||||||
|
|
||||||
export function makeFluent<const Reg extends Registry>(
|
export function makeFluent<const Reg extends Registry>(
|
||||||
registry: Reg,
|
registry: Reg,
|
||||||
) {
|
) {
|
||||||
const fluent = <T>(value: T) => {
|
const fluent = <const T>(value: T) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
const f = { value } as never as Fluent<T, Reg>;
|
const f = { value } as never as Fluent<T, Reg>;
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -8,30 +8,19 @@ export interface Props {
|
|||||||
readonly meta: { registry: Registry };
|
readonly meta: { registry: Registry };
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnionToIntersection<U> = (
|
|
||||||
U extends unknown ? (x: U) => void : never
|
|
||||||
) extends (x: infer I) => void
|
|
||||||
? I
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type MixinHKT = HKT<Props>;
|
type MixinHKT = HKT<Props>;
|
||||||
type MixinFn<T extends MixinHKT> = (
|
type MixinFn = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
fluent: Partial<
|
fluent: Record<PropertyKey, unknown>,
|
||||||
UnionToIntersection<
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
HKT.Apply<T, { value: any; meta: { registry: [] } }>
|
|
||||||
>
|
|
||||||
>,
|
|
||||||
callback: (value: unknown) => never,
|
callback: (value: unknown) => never,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export interface Mixin<T extends MixinHKT = MixinHKT> {
|
export interface Mixin<T extends MixinHKT = MixinHKT> {
|
||||||
hkt: T;
|
hkt: T;
|
||||||
fn: MixinFn<T>;
|
fn: MixinFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Mixin<T extends MixinHKT>(fn: MixinFn<T>): Mixin<T> {
|
export function Mixin<T extends MixinHKT>(fn: MixinFn): Mixin<T> {
|
||||||
return {
|
return {
|
||||||
hkt: never,
|
hkt: never,
|
||||||
fn,
|
fn,
|
||||||
@@ -40,18 +29,26 @@ export function Mixin<T extends MixinHKT>(fn: MixinFn<T>): Mixin<T> {
|
|||||||
|
|
||||||
export namespace Mixin {
|
export namespace Mixin {
|
||||||
export type HKT = MixinHKT;
|
export type HKT = MixinHKT;
|
||||||
export type Function<T extends MixinHKT> = MixinFn<T>;
|
export type Function = MixinFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// type Fluent<T, P extends Props> = _Fluent<
|
export declare const shim: unique symbol;
|
||||||
// T,
|
export interface Shim {
|
||||||
// P["meta"]["registry"]
|
input?: unknown;
|
||||||
// > & {};
|
output?: HKT;
|
||||||
|
value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export type Input<P extends Props> = P["value"];
|
export type Input<P extends Props> = P["value"] extends infer T
|
||||||
|
? T extends { [shim]: { input: infer I } }
|
||||||
|
? I
|
||||||
|
: T
|
||||||
|
: never;
|
||||||
export type Return<T, P extends Props> = Fluent<
|
export type Return<T, P extends Props> = Fluent<
|
||||||
T,
|
P["value"] extends { [shim]: { output: infer U extends HKT } }
|
||||||
P extends { meta: { registry: infer Reg } } ? Reg : never
|
? HKT.Apply<U, T>
|
||||||
|
: T,
|
||||||
|
P["meta"]["registry"]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type Instansiate<
|
export type Instansiate<
|
||||||
|
|||||||
+48
-2
@@ -9,5 +9,51 @@ export const never = undefined as never;
|
|||||||
|
|
||||||
export type Constraint<T, U extends T> = U;
|
export type Constraint<T, U extends T> = U;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
export class AssertionError extends Error {
|
||||||
export type Empty = Constraint<Record<PropertyKey, unknown>, {}>;
|
public constructor(msg?: string) {
|
||||||
|
super(msg);
|
||||||
|
this.name = "AssertionError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assert(
|
||||||
|
condition: unknown,
|
||||||
|
msg?: string,
|
||||||
|
): asserts condition {
|
||||||
|
if (!Boolean(condition))
|
||||||
|
throw new AssertionError(
|
||||||
|
["Assertion error", msg]
|
||||||
|
.filter((v) => v !== undefined)
|
||||||
|
.join(": "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars
|
||||||
|
export function assertType<T>(_v: unknown): asserts _v is T {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
type FunctionProto = Function;
|
||||||
|
export interface HidePrototype {
|
||||||
|
/** @deprecated */
|
||||||
|
apply: FunctionProto["apply"];
|
||||||
|
/** @deprecated */
|
||||||
|
arguments: FunctionProto["arguments"];
|
||||||
|
/** @deprecated */
|
||||||
|
bind: FunctionProto["bind"];
|
||||||
|
/** @deprecated */
|
||||||
|
call: FunctionProto["call"];
|
||||||
|
/** @deprecated */
|
||||||
|
caller: FunctionProto["caller"];
|
||||||
|
/** @deprecated */
|
||||||
|
length: FunctionProto["length"];
|
||||||
|
/** @deprecated */
|
||||||
|
name: FunctionProto["name"];
|
||||||
|
/** @deprecated */
|
||||||
|
prototype: FunctionProto["prototype"];
|
||||||
|
/** @deprecated */
|
||||||
|
toString: FunctionProto["toString"];
|
||||||
|
/** @deprecated */
|
||||||
|
Symbol: SymbolConstructor;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import { makeFluent } from "../base";
|
||||||
|
import type { HKT } from "../base/hkt";
|
||||||
|
import { Mixin, type Input, type Return } from "../base/mixin";
|
||||||
|
import { assert, assertType, type HidePrototype } from "../internal";
|
||||||
|
|
||||||
|
type IterArgs<T extends readonly unknown[] = unknown[]> = [
|
||||||
|
element: T[number],
|
||||||
|
idx: number,
|
||||||
|
array: Readonly<T>,
|
||||||
|
];
|
||||||
|
|
||||||
|
type MaxDepth = 20;
|
||||||
|
|
||||||
|
type Unordered<T extends readonly unknown[]> = T extends unknown[]
|
||||||
|
? T[number][]
|
||||||
|
: readonly T[number][];
|
||||||
|
|
||||||
|
type Map<
|
||||||
|
T extends readonly unknown[],
|
||||||
|
U,
|
||||||
|
TOut extends U[] = [],
|
||||||
|
> = TOut["length"] extends MaxDepth
|
||||||
|
? U[]
|
||||||
|
: T extends readonly [infer T, ...infer TRest extends unknown[]]
|
||||||
|
? Map<TRest, U, [...TOut, U]>
|
||||||
|
: T extends readonly []
|
||||||
|
? TOut
|
||||||
|
: U[];
|
||||||
|
|
||||||
|
type Reverse<
|
||||||
|
T extends readonly unknown[],
|
||||||
|
TOut extends readonly T[number][] = [],
|
||||||
|
> = TOut["length"] extends MaxDepth
|
||||||
|
? T[number][]
|
||||||
|
: T extends readonly [...infer TRest extends unknown[], infer T]
|
||||||
|
? Reverse<TRest, [...TOut, T]>
|
||||||
|
: T extends readonly []
|
||||||
|
? TOut
|
||||||
|
: T[number][];
|
||||||
|
|
||||||
|
namespace At {
|
||||||
|
export type Dec<N extends number> = [
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
][N];
|
||||||
|
|
||||||
|
export type Select<
|
||||||
|
T extends readonly unknown[],
|
||||||
|
Skip extends number,
|
||||||
|
> = Skip extends 0
|
||||||
|
? T extends readonly [...unknown[], infer T]
|
||||||
|
? T
|
||||||
|
: undefined
|
||||||
|
: T extends readonly [...infer TRest, unknown]
|
||||||
|
? Select<TRest, Dec<Skip>>
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type At<
|
||||||
|
T extends readonly unknown[],
|
||||||
|
K extends keyof T & number,
|
||||||
|
> = `${K}` extends `-${infer N extends number}`
|
||||||
|
? At.Select<T, At.Dec<N>>
|
||||||
|
: T[K];
|
||||||
|
|
||||||
|
namespace Flat {
|
||||||
|
type Inc<
|
||||||
|
N extends number,
|
||||||
|
Acc extends unknown[] = [],
|
||||||
|
> = Acc["length"] extends N
|
||||||
|
? [...Acc, null]["length"]
|
||||||
|
: Inc<N, [...Acc, null]>;
|
||||||
|
|
||||||
|
export type Leaves<
|
||||||
|
T,
|
||||||
|
Depth extends number = 0,
|
||||||
|
> = Depth extends MaxDepth
|
||||||
|
? T
|
||||||
|
: T extends readonly unknown[]
|
||||||
|
? Leaves<T[number], Inc<Depth>>
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type Rebuild<
|
||||||
|
T extends readonly unknown[],
|
||||||
|
Depth extends number = 0,
|
||||||
|
> = Depth extends MaxDepth
|
||||||
|
? T
|
||||||
|
: T extends readonly [infer H, ...infer R]
|
||||||
|
? H extends readonly unknown[]
|
||||||
|
? [...Rebuild<H, Inc<Depth>>, ...Rebuild<R, Depth>]
|
||||||
|
: [H, ...Rebuild<R, Depth>]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Flat<T extends readonly unknown[]> =
|
||||||
|
number extends T["length"] ? Flat.Leaves<T>[] : Flat.Rebuild<T>;
|
||||||
|
|
||||||
|
function isArray(v: unknown): v is readonly unknown[] {
|
||||||
|
return typeof v === "object" && globalThis.Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Array extends Mixin.HKT {
|
||||||
|
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||||
|
? T extends readonly (infer Item)[]
|
||||||
|
? {
|
||||||
|
at: <const K extends keyof T & number>(
|
||||||
|
index: K,
|
||||||
|
) => Return<At<T, K>, typeof t>;
|
||||||
|
each: (
|
||||||
|
callback: (...args: IterArgs<T>) => void,
|
||||||
|
) => Return<T, typeof t>;
|
||||||
|
map: <U>(
|
||||||
|
callback: (...args: IterArgs<T>) => U,
|
||||||
|
) => Return<Map<T, U>, typeof t>;
|
||||||
|
filter: (
|
||||||
|
callback: (...args: IterArgs<T>) => boolean,
|
||||||
|
) => Return<Unordered<T>, typeof t>;
|
||||||
|
reduce: (<U>(
|
||||||
|
initial: U,
|
||||||
|
callback: (
|
||||||
|
value: U,
|
||||||
|
...args: IterArgs<T>
|
||||||
|
) => U,
|
||||||
|
) => Return<U, typeof t>) & {
|
||||||
|
right: <U>(
|
||||||
|
initial: U,
|
||||||
|
callback: (
|
||||||
|
value: U,
|
||||||
|
...args: IterArgs<T>
|
||||||
|
) => U,
|
||||||
|
) => Return<U, typeof t>;
|
||||||
|
} & HidePrototype;
|
||||||
|
length: () => Return<T["length"], typeof t>;
|
||||||
|
sort: ((
|
||||||
|
callback: (
|
||||||
|
a: Item,
|
||||||
|
b: Item,
|
||||||
|
arr: Readonly<T>,
|
||||||
|
) => number,
|
||||||
|
) => Return<Unordered<T>, typeof t>) &
|
||||||
|
([Item] extends [string]
|
||||||
|
? {
|
||||||
|
alpha: () => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
alpha: (
|
||||||
|
via: (v: Item) => string,
|
||||||
|
) => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
}) &
|
||||||
|
([Item] extends [number]
|
||||||
|
? {
|
||||||
|
ascending: () => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
descending: () => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ascending: (
|
||||||
|
via: (v: Item) => number,
|
||||||
|
) => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
descending: (
|
||||||
|
via: (v: Item) => number,
|
||||||
|
) => Return<
|
||||||
|
Unordered<T>,
|
||||||
|
typeof t
|
||||||
|
>;
|
||||||
|
}) &
|
||||||
|
HidePrototype;
|
||||||
|
reverse: () => Return<Reverse<T>, typeof t>;
|
||||||
|
}
|
||||||
|
: unknown
|
||||||
|
: never;
|
||||||
|
}
|
||||||
|
export const Array = Mixin<Array>((value, $, fluent) => {
|
||||||
|
if (!isArray(value)) return;
|
||||||
|
|
||||||
|
$.at = (index: number) => {
|
||||||
|
return fluent(value.at(index));
|
||||||
|
};
|
||||||
|
|
||||||
|
$.each = (callback: (...args: IterArgs) => void) => {
|
||||||
|
value.forEach(callback);
|
||||||
|
return fluent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.map = (callback: (...args: IterArgs) => unknown) => {
|
||||||
|
return fluent(value.map(callback));
|
||||||
|
};
|
||||||
|
|
||||||
|
$.filter = (callback: (...args: IterArgs) => boolean) => {
|
||||||
|
return fluent(value.filter(callback));
|
||||||
|
};
|
||||||
|
|
||||||
|
$.reduce = (
|
||||||
|
initial: unknown,
|
||||||
|
callback: (value: unknown, ...args: IterArgs) => unknown,
|
||||||
|
) => {
|
||||||
|
return fluent(value.reduce(callback, initial));
|
||||||
|
};
|
||||||
|
assertType<object>($.reduce);
|
||||||
|
Object.assign($.reduce, {
|
||||||
|
right: (
|
||||||
|
initial: unknown,
|
||||||
|
callback: (value: unknown, ...args: IterArgs) => unknown,
|
||||||
|
) => {
|
||||||
|
return fluent(value.reduceRight(callback, initial));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$.length = () => {
|
||||||
|
return fluent(value.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.sort = (
|
||||||
|
callback: (
|
||||||
|
a: unknown,
|
||||||
|
b: unknown,
|
||||||
|
arr: readonly unknown[],
|
||||||
|
) => number,
|
||||||
|
) => {
|
||||||
|
return fluent(
|
||||||
|
value.toSorted((a, b) => callback(a, b, value)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
assertType<object>($.sort);
|
||||||
|
Object.assign($.sort, {
|
||||||
|
alpha: (
|
||||||
|
via: (v: unknown) => string = (v) => (
|
||||||
|
assert(typeof v === "string"),
|
||||||
|
v
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
return fluent(
|
||||||
|
value.toSorted((a, b) =>
|
||||||
|
via(a)! < via(b)! ? -1 : 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ascending: (
|
||||||
|
via: (v: unknown) => number = (v) => (
|
||||||
|
assert(typeof v === "number"),
|
||||||
|
v
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
return fluent(value.toSorted((a, b) => via(a) - via(b)));
|
||||||
|
},
|
||||||
|
descending: (
|
||||||
|
via: (v: unknown) => number = (v) => (
|
||||||
|
assert(typeof v === "number"),
|
||||||
|
v
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
return fluent(value.toSorted((a, b) => via(b) - via(a)));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$.reverse = () => {
|
||||||
|
return fluent(value.toReversed());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.vitest) {
|
||||||
|
const { test, expect, vi } = import.meta.vitest;
|
||||||
|
|
||||||
|
const registry = [Array] as const;
|
||||||
|
const $ = makeFluent(registry);
|
||||||
|
|
||||||
|
test("at()", () => {
|
||||||
|
const arr = [1, 2, 3] as const;
|
||||||
|
|
||||||
|
expect($(arr).at(0).value).toBe(arr[0]);
|
||||||
|
expect($(arr).at(5).value).toBe(undefined);
|
||||||
|
|
||||||
|
expect($(arr).at(-1).value).toBe(arr[arr.length - 1]);
|
||||||
|
expect($(arr).at(-5).value).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("each()", () => {
|
||||||
|
const arr = [5, 7, 1] as const;
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
const result = $(arr).each(callback).value;
|
||||||
|
|
||||||
|
expect(result).toMatchObject(arr);
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++)
|
||||||
|
expect(callback).toHaveBeenNthCalledWith(
|
||||||
|
i + 1,
|
||||||
|
arr[i],
|
||||||
|
i,
|
||||||
|
arr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("map()", () => {
|
||||||
|
const arr = [1, 2, 3, 4, 5] as const;
|
||||||
|
const isEven = vi.fn((v: number) => v % 2 === 0);
|
||||||
|
|
||||||
|
expect($(arr).map(isEven).value).toMatchObject(
|
||||||
|
arr.map(isEven),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++)
|
||||||
|
expect(isEven).toHaveBeenNthCalledWith(
|
||||||
|
i + 1,
|
||||||
|
arr[i],
|
||||||
|
i,
|
||||||
|
arr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filter()", () => {
|
||||||
|
const arr = [5, 4, 3, 2, 1] as const;
|
||||||
|
const isEven = vi.fn((v: number) => v % 2 === 0);
|
||||||
|
|
||||||
|
expect($(arr).filter(isEven).value).toMatchObject(
|
||||||
|
arr.filter(isEven),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++)
|
||||||
|
expect(isEven).toHaveBeenNthCalledWith(
|
||||||
|
i + 1,
|
||||||
|
arr[i],
|
||||||
|
i,
|
||||||
|
arr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reduce()", () => {
|
||||||
|
const arr = [5, 5, 5, 5, 5] as const;
|
||||||
|
|
||||||
|
const callback = vi.fn((a: number, b: number) => a + b);
|
||||||
|
const expected = arr.reduce(callback, 0);
|
||||||
|
|
||||||
|
callback.mockClear();
|
||||||
|
|
||||||
|
expect($(arr).reduce(0, callback).value).toBe(expected);
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
expect(callback).toHaveBeenNthCalledWith(
|
||||||
|
i + 1,
|
||||||
|
sum,
|
||||||
|
arr[i],
|
||||||
|
i,
|
||||||
|
arr,
|
||||||
|
);
|
||||||
|
sum += arr[i]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.mockClear();
|
||||||
|
|
||||||
|
expect($(arr).reduce.right(0, callback).value).toBe(expected);
|
||||||
|
|
||||||
|
sum = 0;
|
||||||
|
for (let i = arr.length; i > 0; i--) {
|
||||||
|
expect(callback).toHaveBeenNthCalledWith(
|
||||||
|
arr.length - i + 1,
|
||||||
|
sum,
|
||||||
|
arr[i - 1],
|
||||||
|
i - 1,
|
||||||
|
arr,
|
||||||
|
);
|
||||||
|
sum += arr[i - 1]!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("length()", () => {
|
||||||
|
const arr = [3, 3, 3] as const;
|
||||||
|
|
||||||
|
expect($(arr).length().value).toBe(arr.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sort()", () => {
|
||||||
|
const numbers = [1, 5, 3, 4, 2] as const;
|
||||||
|
|
||||||
|
const callback = (a: number) => (a % 2 === 0 ? -1 : 1);
|
||||||
|
|
||||||
|
expect($(numbers).sort(callback).value).toMatchObject(
|
||||||
|
numbers.toSorted(callback),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($(numbers).sort.ascending().value).toMatchObject(
|
||||||
|
numbers.toSorted((a, b) => a - b),
|
||||||
|
);
|
||||||
|
expect($(numbers).sort.descending().value).toMatchObject(
|
||||||
|
numbers.toSorted((a, b) => b - a),
|
||||||
|
);
|
||||||
|
|
||||||
|
const boxed = [{ v: 3 }, { v: 2 }, { v: 1 }];
|
||||||
|
expect(
|
||||||
|
$(boxed).sort.ascending((v) => v.v).value,
|
||||||
|
).toMatchObject(boxed.toSorted((a, b) => a.v - b.v));
|
||||||
|
expect(
|
||||||
|
$(boxed).sort.descending((v) => v.v).value,
|
||||||
|
).toMatchObject(boxed.toSorted((a, b) => b.v - a.v));
|
||||||
|
|
||||||
|
const strings = [
|
||||||
|
"beta",
|
||||||
|
"alpha",
|
||||||
|
"bart",
|
||||||
|
"apple",
|
||||||
|
"charlie",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
expect($(strings).sort.alpha().value).toMatchObject(
|
||||||
|
strings.toSorted(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
+106
-17
@@ -1,47 +1,136 @@
|
|||||||
import { makeFluent } from "../base";
|
import { makeFluent } from "../base";
|
||||||
import type { HKT } from "../base/hkt";
|
import type { HKT } from "../base/hkt";
|
||||||
import {
|
import { Mixin, shim, type Input, type Return } from "../base/mixin";
|
||||||
Mixin,
|
import { assert, assertType, never } from "../internal";
|
||||||
type Input,
|
|
||||||
type Props,
|
|
||||||
type Return,
|
|
||||||
} from "../base/mixin";
|
|
||||||
import type { Empty } from "../internal";
|
|
||||||
import type { Methods } from "../registry";
|
|
||||||
import { Base } from "./base";
|
import { Base } from "./base";
|
||||||
|
|
||||||
|
interface AwaitedIdentitity extends HKT {
|
||||||
|
new: (t: HKT.T<this>) => Awaited<Promise<typeof t>>;
|
||||||
|
}
|
||||||
|
|
||||||
class Awaited<T extends Promise<unknown>> {
|
class Awaited<T extends Promise<unknown>> {
|
||||||
public value: T;
|
public value: T;
|
||||||
public constructor(value: T) {
|
public constructor(value: T) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get [shim]() {
|
||||||
|
return never as {
|
||||||
|
input: T extends Promise<infer U> ? U : never;
|
||||||
|
output: AwaitedIdentitity;
|
||||||
|
value: T;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AwaitedMixin extends Mixin.HKT {
|
export interface AsyncMixin extends Mixin.HKT {
|
||||||
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||||
? T extends Promise<unknown>
|
? T extends Promise<unknown>
|
||||||
? {
|
? {
|
||||||
readonly awaited: Return<Awaited<T>, typeof t>;
|
readonly awaited: Return<Awaited<T>, typeof t>;
|
||||||
|
then: <U>(
|
||||||
|
callback: (
|
||||||
|
value: T extends Promise<infer T>
|
||||||
|
? T
|
||||||
|
: never,
|
||||||
|
) => U,
|
||||||
|
) => Return<Promise<U>, typeof t>;
|
||||||
}
|
}
|
||||||
: 0
|
: unknown
|
||||||
: never;
|
: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AwaitedMixin = Mixin<AwaitedMixin>(
|
interface BaseFluent<T> {
|
||||||
(value, $, fluent) => {},
|
value: T;
|
||||||
);
|
[K: PropertyKey]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
|
||||||
|
if (!(value instanceof Promise)) return;
|
||||||
|
|
||||||
|
$.then = (callback: (value: unknown) => unknown) => {
|
||||||
|
return fluent(value.then(callback));
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty($, "awaited", {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
let v: Promise<BaseFluent<unknown>> = value.then((v) =>
|
||||||
|
fluent(v),
|
||||||
|
);
|
||||||
|
|
||||||
|
const path: PropertyKey[] = [];
|
||||||
|
// eslint-disable-next-line no-empty-pattern
|
||||||
|
const proxy = new Proxy((...[]: unknown[]) => proxy, {
|
||||||
|
get: (_, key) => {
|
||||||
|
if (key === "value")
|
||||||
|
return v.then((v) => v.value);
|
||||||
|
path.push(key);
|
||||||
|
return proxy;
|
||||||
|
},
|
||||||
|
apply: (target, thisArg, args: unknown[]) => {
|
||||||
|
v = v.then((v) => {
|
||||||
|
let obj: unknown = v;
|
||||||
|
for (const node of path) {
|
||||||
|
assert(
|
||||||
|
typeof obj === "object" &&
|
||||||
|
obj !== null &&
|
||||||
|
node in obj,
|
||||||
|
);
|
||||||
|
assertType<Record<PropertyKey, unknown>>(
|
||||||
|
obj,
|
||||||
|
);
|
||||||
|
|
||||||
|
obj = obj[node];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(typeof obj === "function");
|
||||||
|
assertType<
|
||||||
|
(
|
||||||
|
...args: unknown[]
|
||||||
|
) => BaseFluent<unknown>
|
||||||
|
>(obj);
|
||||||
|
|
||||||
|
return obj(...args);
|
||||||
|
});
|
||||||
|
return target.apply(thisArg, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (import.meta.vitest) {
|
if (import.meta.vitest) {
|
||||||
const { test, expect } = import.meta.vitest;
|
const { test, expect } = import.meta.vitest;
|
||||||
|
|
||||||
const registry = [Base, AwaitedMixin] as const;
|
const registry = [Base, AsyncMixin] as const;
|
||||||
const $ = makeFluent(registry);
|
const $ = makeFluent(registry);
|
||||||
|
|
||||||
test(".awaited", () => {
|
test(".awaited", async () => {
|
||||||
|
const value = 10;
|
||||||
const promise = new Promise<number>((r) => {
|
const promise = new Promise<number>((r) => {
|
||||||
r(10);
|
r(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const m = $(promise).V;
|
const increment = (n: number) => n + 1;
|
||||||
|
|
||||||
|
const result = $(promise).awaited.transform(increment).value;
|
||||||
|
|
||||||
|
expect(await result).toBe(increment(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("then()", async () => {
|
||||||
|
const value = 10;
|
||||||
|
const promise = new Promise<number>((r) => {
|
||||||
|
r(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const increment = (n: number) => n + 1;
|
||||||
|
|
||||||
|
const result = $(promise).then(increment).value;
|
||||||
|
|
||||||
|
expect(await result).toBe(increment(value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -1,7 +1,6 @@
|
|||||||
import { makeFluent } from "../base";
|
import { makeFluent } from "../base";
|
||||||
import type { HKT } from "../base/hkt";
|
import type { HKT } from "../base/hkt";
|
||||||
import { Mixin, type Input, type Return } from "../base/mixin";
|
import { Mixin, type Input, type Return } from "../base/mixin";
|
||||||
import type { Empty } from "../internal";
|
|
||||||
|
|
||||||
export interface Base extends Mixin.HKT {
|
export interface Base extends Mixin.HKT {
|
||||||
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||||
@@ -12,18 +11,17 @@ export interface Base extends Mixin.HKT {
|
|||||||
transform<U>(
|
transform<U>(
|
||||||
callback: (value: T) => U,
|
callback: (value: T) => U,
|
||||||
): Return<U, typeof t>;
|
): Return<U, typeof t>;
|
||||||
readonly V: Return<number, typeof t>;
|
|
||||||
}
|
}
|
||||||
: never;
|
: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Base = Mixin<Base>((value, $, fluent) => {
|
export const Base = Mixin<Base>((value, $, fluent) => {
|
||||||
$.tap = (callback) => {
|
$.tap = (callback: (value: unknown) => void) => {
|
||||||
callback(value);
|
callback(value);
|
||||||
return fluent(value);
|
return fluent(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
$.transform = (callback) => {
|
$.transform = (callback: (value: unknown) => unknown) => {
|
||||||
return fluent(callback(value));
|
return fluent(callback(value));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
export { Base } from "./base";
|
export { Base } from "./base";
|
||||||
export { AwaitedMixin as Awaited } from "./awaited";
|
export { AsyncMixin as Async } from "./awaited";
|
||||||
|
export { Optional } from "./optional";
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { makeFluent } from "../base";
|
||||||
|
import type { HKT } from "../base/hkt";
|
||||||
|
import {
|
||||||
|
Mixin,
|
||||||
|
type Input,
|
||||||
|
type Props,
|
||||||
|
type Return,
|
||||||
|
} from "../base/mixin";
|
||||||
|
import { assertType, type HidePrototype } from "../internal";
|
||||||
|
|
||||||
|
type NoneSentinel = null | undefined;
|
||||||
|
type None<T> = Extract<T, NoneSentinel>;
|
||||||
|
type Some<T> = Exclude<T, NoneSentinel>;
|
||||||
|
|
||||||
|
function isNone<T>(v: T): v is None<T> {
|
||||||
|
return v === null || v === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Where<T, t extends Props> {
|
||||||
|
where: <U = null>(
|
||||||
|
callback: (v: T) => boolean,
|
||||||
|
fallback?: U,
|
||||||
|
) => Return<T | U, t>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Optional extends Mixin.HKT {
|
||||||
|
new: (t: HKT.T<this>) => Input<typeof t> extends infer T
|
||||||
|
? None<T> extends never
|
||||||
|
? Where<T, typeof t>
|
||||||
|
: {
|
||||||
|
and: <U>(
|
||||||
|
callback: (v: Some<T>) => U,
|
||||||
|
) => Return<None<T> | U, typeof t>;
|
||||||
|
or: (<U>(
|
||||||
|
fallback: U,
|
||||||
|
) => Return<Some<T> | U, typeof t>) & {
|
||||||
|
else: <U>(
|
||||||
|
callback: (v: None<T>) => U,
|
||||||
|
) => Return<Some<T> | U, typeof t>;
|
||||||
|
} & HidePrototype;
|
||||||
|
} & Where<T, typeof t>
|
||||||
|
: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Optional = Mixin<Optional>((value, $, fluent) => {
|
||||||
|
if (isNone(value)) {
|
||||||
|
$.and = () => {
|
||||||
|
return fluent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.or = (fallback: unknown) => {
|
||||||
|
return fluent(fallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
assertType<object>($.or);
|
||||||
|
Object.assign($.or, {
|
||||||
|
else: (callback: (v: unknown) => unknown) => {
|
||||||
|
return fluent(callback(value));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.and = (callback: (v: unknown) => unknown) => {
|
||||||
|
return fluent(callback(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
$.or = () => {
|
||||||
|
return fluent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
assertType<object>($.or);
|
||||||
|
Object.assign($.or, {
|
||||||
|
else: () => {
|
||||||
|
return fluent(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.where = (
|
||||||
|
callback: (v: unknown) => boolean,
|
||||||
|
fallback = null,
|
||||||
|
) => {
|
||||||
|
if (callback(value)) return fluent(value);
|
||||||
|
return fluent(fallback);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.vitest) {
|
||||||
|
const { test, expect } = import.meta.vitest;
|
||||||
|
|
||||||
|
const registry = [Optional] as const;
|
||||||
|
const $ = makeFluent(registry);
|
||||||
|
|
||||||
|
type Value = number | null;
|
||||||
|
|
||||||
|
test("and()", () => {
|
||||||
|
const value = 10;
|
||||||
|
const some = value as Value;
|
||||||
|
const none = null as Value;
|
||||||
|
|
||||||
|
const callback = (v: number) => v + 5;
|
||||||
|
|
||||||
|
expect($(some).and(callback).value).toBe(callback(value));
|
||||||
|
expect($(none).and(callback).value).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("or()", () => {
|
||||||
|
const some = 10 as Value;
|
||||||
|
const none = null as Value;
|
||||||
|
|
||||||
|
const fallback = 15 as const;
|
||||||
|
|
||||||
|
expect($(some).or(fallback).value).toBe(some);
|
||||||
|
expect($(none).or(fallback).value).toBe(fallback);
|
||||||
|
|
||||||
|
const callback = () => fallback;
|
||||||
|
|
||||||
|
expect($(some).or.else(callback).value).toBe(some);
|
||||||
|
expect($(none).or.else(callback).value).toBe(fallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("where()", () => {
|
||||||
|
const even = 4;
|
||||||
|
const odd = 7;
|
||||||
|
|
||||||
|
const isEven = (v: number) => v % 2 === 0;
|
||||||
|
|
||||||
|
expect($(even).where(isEven).value).toBe(even);
|
||||||
|
expect($(odd).where(isEven).value).toBe(null);
|
||||||
|
|
||||||
|
expect($(odd).where(isEven, -1).value).toBe(-1);
|
||||||
|
});
|
||||||
|
}
|
||||||
+7
-8
@@ -1,26 +1,25 @@
|
|||||||
import { Mixin, type Instansiate } from "./base/mixin";
|
import { Mixin, type Instansiate } from "./base/mixin";
|
||||||
import type { Empty } from "./internal";
|
import { Async, Base, Optional } from "./mixin";
|
||||||
import { Base, Awaited } from "./mixin";
|
import { Array } from "./mixin/array";
|
||||||
|
|
||||||
export type Registry = readonly Mixin[];
|
export type Registry = readonly Mixin[];
|
||||||
|
|
||||||
export const DEFAULT_REGISTRY = [
|
export const DEFAULT_REGISTRY = [
|
||||||
|
Async,
|
||||||
|
Array,
|
||||||
Base,
|
Base,
|
||||||
Awaited,
|
Optional,
|
||||||
] as const satisfies Registry;
|
] as const satisfies Registry;
|
||||||
export type DefaultRegistry = typeof DEFAULT_REGISTRY;
|
export type DefaultRegistry = typeof DEFAULT_REGISTRY;
|
||||||
|
|
||||||
type Expand<T> = { [K in keyof T]: T[K] };
|
|
||||||
type Merge<T, U> = [T] extends [0] ? U : [U] extends [0] ? T : T & U;
|
|
||||||
|
|
||||||
export type Methods<
|
export type Methods<
|
||||||
T,
|
T,
|
||||||
Reg extends Registry,
|
Reg extends Registry,
|
||||||
TReg extends Registry = Reg,
|
TReg extends Registry = Reg,
|
||||||
TOut = {},
|
TOut extends Record<PropertyKey, unknown> = Record<never, never>,
|
||||||
> = TReg extends readonly [
|
> = TReg extends readonly [
|
||||||
infer M extends Mixin,
|
infer M extends Mixin,
|
||||||
...infer Rest extends Registry,
|
...infer Rest extends Registry,
|
||||||
]
|
]
|
||||||
? Methods<T, Reg, Rest, Merge<TOut, Instansiate<M, T, Reg>>>
|
? Methods<T, Reg, Rest, TOut & Instansiate<M, T, Reg>>
|
||||||
: TOut;
|
: TOut;
|
||||||
|
|||||||
Reference in New Issue
Block a user