Skip to content

Commit 4f9eb6d

Browse files
committed
✨ add @coven/utils
1 parent ea5da81 commit 4f9eb6d

25 files changed

+356
-1
lines changed

β€Ž@coven/utils/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<img alt="Coven Engineering Types logo" src="https://raw.githubusercontent.com/covenengineering/libraries/main/@coven/utils/logo.svg" height="108" />
2+
3+
πŸͺ„ Utility spells.

β€Ž@coven/utils/always.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { identity } from "./identity.ts";
2+
import { thunk } from "./thunk.ts";
3+
4+
/**
5+
* Returns a function that always returns the same value.
6+
*
7+
* @category Functions
8+
* @example
9+
* ```typescript
10+
* const alwaysFoo = always("foo");
11+
* const fillWithFoo = map(alwaysFoo);
12+
*
13+
* fillWithFoo([0, 1, 2]); // ["foo", "foo", "foo"]
14+
* ```
15+
* @returns Function that always return the given value.
16+
*/
17+
export const always = thunk(identity);

β€Ž@coven/utils/applyTo.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Unary } from "@coven/types";
2+
3+
/**
4+
* Takes a value and applies a function to it.
5+
*
6+
* @category Functions
7+
* @example
8+
* ```typescript
9+
* const applyTo10 = applyTo(10);
10+
*
11+
* applyTo10((value: number) => value * 2); // 20
12+
* applyTo10((value: number) => value / 2); // 5
13+
* ```
14+
* @returns Function that expects a function that will receive the `input`.
15+
*/
16+
export const applyTo = <const Input>(
17+
input: Input,
18+
): <const Output>(unary: Unary<[input: Input], Output>) => Output =>
19+
(unary) => unary(input);

β€Ž@coven/utils/cryptoNumber.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const textEncoder = new TextEncoder();
2+
const LITTLE_ENDIAN = true;
3+
const sha256ToNumber = (sha256: ArrayBuffer) =>
4+
new DataView(sha256).getUint32(0, LITTLE_ENDIAN) / 0xff_ff_ff_ff;
5+
6+
/**
7+
* Generates `number` using `SubtleCrypto#digest` and the given seed.
8+
*
9+
* **⚠️ IMPORTANT:** This only works in secure contexts.
10+
*
11+
* @category Numbers
12+
* @example
13+
* ```typescript
14+
* const seededRandom1 = await random("some seed");
15+
* const seededRandom2 = await random("some seed");
16+
*
17+
* seededRandom1 === seededRandom2; // true because it has the same seed
18+
* ```
19+
* @see [SubtleCrypto#digest](https://mdn.io/SubtleCrypto.digest)
20+
* @param seed Seed to be used to generate random numbers.
21+
* @returns Pseudo-random number from seed.
22+
*/
23+
export const cryptoNumber = (seed: string): Promise<number> =>
24+
crypto.subtle.digest("SHA-256", textEncoder.encode(seed)).then(
25+
sha256ToNumber,
26+
);

β€Ž@coven/utils/deno.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "@coven/utils",
3+
"version": "0.0.5",
4+
"exports": "./mod.ts"
5+
}

β€Ž@coven/utils/get.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ReadonlyRecord } from "@coven/types";
2+
3+
/**
4+
* Get the value of a property in an object.
5+
*
6+
* @category Objects
7+
* @example
8+
* ```typescript
9+
* const getFoo = get("foo");
10+
*
11+
* getFoo({ foo: "bar" }); // "bar"
12+
* getFoo({}); // undefined
13+
* ```
14+
* @returns Curried function with `key` in context.
15+
*/
16+
export const get = <const Key extends PropertyKey>(
17+
key: Key,
18+
): <const Source extends ReadonlyRecord<Key>>(
19+
object: Source,
20+
) => Source[Key & keyof Source] =>
21+
(object) => object[key];

β€Ž@coven/utils/identity.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Identity function.
3+
*
4+
* @category Functions
5+
* @example
6+
* ```typescript
7+
* identity("foo"); // "foo"
8+
* ```
9+
* @returns Same value given.
10+
*/
11+
export const identity = <const Input>(input: Input): Input => input;

β€Ž@coven/utils/logo.svg

+1
Loading

β€Ž@coven/utils/memoize.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Unary } from "@coven/types";
2+
3+
/**
4+
* Memoize function return values for expensive operations.
5+
*
6+
* @category Functions
7+
* @example
8+
* ```typescript
9+
* const expensiveOperation = (value: number) => value * 2;
10+
* const memoizedOperation = memoize(expensiveOperation);
11+
*
12+
* memoizedOperation(2); // 4
13+
* memoizedOperation(2); // 4 (cached)
14+
* ```
15+
* @param unary Function to memoize.
16+
* @returns Curried function with `unary` in context.
17+
*/
18+
export const memoize = <
19+
MemoizedFunction extends Unary<[value: never], unknown>,
20+
>(
21+
unary: MemoizedFunction,
22+
): MemoizedFunction => {
23+
const cache = new Map<
24+
Parameters<MemoizedFunction>[0],
25+
ReturnType<MemoizedFunction>
26+
>();
27+
28+
return ((input: Parameters<MemoizedFunction>[0]) =>
29+
cache.get(input) ??
30+
cache
31+
.set(
32+
input,
33+
unary(input as never) as ReturnType<MemoizedFunction>,
34+
)
35+
.get(input)) as unknown as MemoizedFunction;
36+
};

β€Ž@coven/utils/mod.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { always } from "./always.ts";
2+
export { applyTo } from "./applyTo.ts";
3+
export { cryptoNumber } from "./cryptoNumber.ts";
4+
export { get } from "./get.ts";
5+
export { identity } from "./identity.ts";
6+
export { memoize } from "./memoize.ts";
7+
export { mutate } from "./mutate.ts";
8+
export { set } from "./set.ts";
9+
export { tap } from "./tap.ts";
10+
export { thunk } from "./thunk.ts";

β€Ž@coven/utils/mutate.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Function to encapsulate object mutations.
3+
*
4+
* @category Objects
5+
* @example
6+
* ```typescript
7+
* const state = { a: 1 };
8+
* mutate(set("a")(2))(state);
9+
* console.log(state); // { a: 2 }
10+
* ```
11+
* @param update Update to apply to given target.
12+
* @returns Curried function with `update` in context.
13+
*/
14+
export const mutate = <const Update extends object>(
15+
update: Update,
16+
): <const Target extends object>(target: Target) => Target & Update =>
17+
(target) => Object.assign(target, update);

β€Ž@coven/utils/set.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Set the value of a property in an object (read only).
3+
*
4+
* @category Objects
5+
* @example
6+
* ```typescript
7+
* const setFoo = set("foo");
8+
*
9+
* setFoo("baz")({ foo: "bar" }); // { foo: "baz" }
10+
* setFoo("baz")({ bar: "foo" }); // { bar: "foo", foo: "baz" }
11+
* setFoo("baz")({}); // { foo: "baz" }
12+
* ```
13+
* @returns Curried function with `key` in context.
14+
*/
15+
export const set = <const Key extends PropertyKey>(
16+
key: Key,
17+
): <const Value>(
18+
value: Value,
19+
) => <const Source extends object>(
20+
object: Source,
21+
) => Omit<Source, Key> & Record<Key, Value> =>
22+
<const Value>(value: Value) =>
23+
<const Source extends object>(object: Source) =>
24+
({ ...object, [key]: value }) as Omit<Source, Key> & Record<Key, Value>;

β€Ž@coven/utils/tap.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Unary } from "@coven/types";
2+
3+
/**
4+
* Tap into a value before calling a function.
5+
*
6+
* @category Functions
7+
* @example
8+
* ```typescript
9+
* const log = tap(console.log);
10+
* const double = (value: number) => value * 2;
11+
* const doubleAndLog = log(double);
12+
*
13+
* doubleAndLog(2); // 4 (returns and logs 4)
14+
* ```
15+
* @param tapper Tapper function to be called with the value.
16+
* @returns Curried function with `tapper` in context.
17+
*/
18+
export const tap = <Input, Output>(
19+
tapper: Unary<[input: Input], Output>,
20+
): <Tapped extends Unary<[input: Input], void>>(
21+
tapped: Tapped,
22+
) => Unary<[input: Input], Output> =>
23+
(tapped) =>
24+
(input) => (tapper(input), tapped(input)) as Output;

β€Ž@coven/utils/thunk.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Unary } from "@coven/types";
2+
3+
/**
4+
* Delayed evaluation function.
5+
*
6+
* @category Functions
7+
* @example
8+
* ```typescript
9+
* const always = thunk(id);
10+
* const alwaysFoo = always("foo")
11+
* alwaysFoo(); // "foo"
12+
* ```
13+
* @returns Function that will run the given function when called.
14+
*/
15+
export const thunk = <Input, Output>(
16+
unary: Unary<[input: Input], Output>,
17+
): (input: Input) => () => Output =>
18+
(input) =>
19+
() => unary(input);

β€Ždeno.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"./@coven/parsers",
138138
"./@coven/predicates",
139139
"./@coven/terminal",
140-
"./@coven/types"
140+
"./@coven/types",
141+
"./@coven/utils"
141142
]
142143
}

β€Žtests/@coven/utils/always.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { always } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
Deno.test("always with a string returns said string when called", () =>
5+
assertEquals(always("πŸ§™πŸ»β€β™€οΈ")(), "πŸ§™πŸ»β€β™€οΈ"));
6+
7+
Deno.test("an array and an always with a string return array filled with string", () =>
8+
assertEquals([0, 1, 2, 3].map(always("πŸ§™πŸ»β€β™€οΈ")), [
9+
"πŸ§™πŸ»β€β™€οΈ",
10+
"πŸ§™πŸ»β€β™€οΈ",
11+
"πŸ§™πŸ»β€β™€οΈ",
12+
"πŸ§™πŸ»β€β™€οΈ",
13+
]));

β€Žtests/@coven/utils/applyTo.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { applyTo } from "@coven/utils";
2+
import { assertStrictEquals } from "@std/assert";
3+
4+
const double = (value: number) => value * 2;
5+
const applyTo21 = applyTo(21);
6+
7+
Deno.test("applyTo with a number and a duplicate function returns double of number", () =>
8+
assertStrictEquals(applyTo21(double), 42));
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { cryptoNumber } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
Deno.test('1 cryptoNumber call with a "test" seed returns the same result', () =>
5+
cryptoNumber("test").then((result) =>
6+
assertEquals(result, 0.507_088_102_285_537_9)
7+
));
8+
9+
Deno.test('2 cryptoNumber call with a "test" seed returns the same for both', () =>
10+
Promise.all([cryptoNumber("test"), cryptoNumber("test")]).then((result) =>
11+
assertEquals(result, [
12+
0.507_088_102_285_537_9,
13+
0.507_088_102_285_537_9,
14+
])
15+
));

β€Žtests/@coven/utils/get.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { get } from "@coven/utils";
2+
import { assertStrictEquals } from "@std/assert";
3+
4+
const getWitch = get("πŸ§™πŸ»β€β™€οΈ");
5+
6+
const EXPECTED = true;
7+
8+
Deno.test("Getter and an object with that property on it returns property value", () =>
9+
assertStrictEquals(getWitch({ "πŸ§™πŸ»β€β™€οΈ": EXPECTED }), EXPECTED));
10+
11+
Deno.test("Getter and an object without that property on it returns property value", () =>
12+
assertStrictEquals(
13+
getWitch({} as { readonly "πŸ§™πŸ»β€β™€οΈ": boolean }),
14+
undefined,
15+
));
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { identity } from "@coven/utils";
2+
import { assertStrictEquals } from "@std/assert";
3+
4+
const anObject = { lucky: 13 };
5+
6+
Deno.test("Identity returns the same string it receives", () =>
7+
assertStrictEquals(identity("πŸ§™πŸ»β€β™€οΈ"), "πŸ§™πŸ»β€β™€οΈ"));
8+
9+
Deno.test("Identity returns the same object it receives, not a copy", () =>
10+
assertStrictEquals(identity(anObject), anObject));

β€Žtests/@coven/utils/memoize.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { memoize } from "@coven/utils";
2+
import { assertStrictEquals } from "@std/assert";
3+
4+
let times = 0;
5+
const double = (value: number) => ((times += 1), value * 2);
6+
const memoizedDouble = memoize(double);
7+
8+
Deno.test("Memoized double function and several operations duplicated values runs once per value", () =>
9+
assertStrictEquals(
10+
([2, 2, 2, 3, 3, 3, 2, 2, 2].map(memoizedDouble), times),
11+
2,
12+
));

β€Žtests/@coven/utils/mutate.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { mutate } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
const mutateWitch = mutate({ "πŸ§™πŸ»β€β™€οΈ": "πŸŽƒ" });
5+
const emptyObject = {};
6+
const witchObject = { "πŸ§™πŸ»β€β™€οΈ": "πŸ§™πŸ»β€β™€οΈ" };
7+
8+
Deno.test('Mutate function that sets a `"πŸ§™πŸ»β€β™€οΈ"` property to `"πŸŽƒ"` and an empty object returns object with added property', () =>
9+
assertEquals((mutateWitch(emptyObject), emptyObject), { "πŸ§™πŸ»β€β™€οΈ": "πŸŽƒ" }));
10+
11+
Deno.test('Mutate function that sets a `"πŸ§™πŸ»β€β™€οΈ"` property to `"πŸŽƒ"` and an object with that property on it returns object with added property', () =>
12+
assertEquals((mutateWitch(witchObject), witchObject), { "πŸ§™πŸ»β€β™€οΈ": "πŸŽƒ" }));

β€Žtests/@coven/utils/set.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { set } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
const SET_TRUE = true;
5+
const setWitch = set("πŸ§™πŸ»β€β™€οΈ")(SET_TRUE);
6+
const existingObject = { "πŸ§™πŸ»β€β™€οΈ": false };
7+
8+
Deno.test("Setter and an object with that property on it returns object with updated property", () =>
9+
assertEquals(setWitch({ "πŸ§™πŸ»β€β™€οΈ": false }), { "πŸ§™πŸ»β€β™€οΈ": true }));
10+
11+
Deno.test("Setter and an object without that property on it returns object with new property", () =>
12+
assertEquals(setWitch({}), { "πŸ§™πŸ»β€β™€οΈ": true }));
13+
14+
Deno.test("Setter doesn't mutate original object", () =>
15+
assertEquals([setWitch(existingObject), existingObject], [{
16+
"πŸ§™πŸ»β€β™€οΈ": true,
17+
}, { "πŸ§™πŸ»β€β™€οΈ": false }]));

β€Žtests/@coven/utils/tap.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { tap } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
let tapped = false;
5+
const tappedTest = tap((argument: boolean) => (tapped = argument))(
6+
(_argument: boolean) => "πŸ§™πŸ»β€β™€οΈ",
7+
);
8+
const EXPECTED = true;
9+
10+
Deno.test("Tapped function returns expected value but runs tapper first", () =>
11+
assertEquals([tappedTest(EXPECTED), tapped], ["πŸ§™πŸ»β€β™€οΈ", EXPECTED]));

β€Žtests/@coven/utils/thunk.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { thunk } from "@coven/utils";
2+
import { assertEquals } from "@std/assert";
3+
4+
const double = (value: number) => value * 2;
5+
const thunkDouble = thunk(double);
6+
7+
Deno.test("Thunk for a double function returns delayed double function", () =>
8+
assertEquals(thunkDouble(21)(), 42));

0 commit comments

Comments
Β (0)