From 6ae2f079259323c5d41b72300ee3256711423886 Mon Sep 17 00:00:00 2001 From: Ignatius Bagus Date: Wed, 3 Jul 2024 13:34:35 +0700 Subject: [PATCH] feat: add `augment` function (#248) --- .../mauss/src/core/standard/index.spec.ts | 2 +- workspace/mauss/src/std/README.md | 37 ++---- workspace/mauss/src/std/array.spec.ts | 57 ---------- workspace/mauss/src/std/array.ts | 15 --- .../src/std/{object.spec.ts => index.spec.ts} | 106 +++++++++++++----- .../src/std/{object.test.ts => index.test.ts} | 8 +- workspace/mauss/src/std/index.ts | 91 ++++++++++++++- workspace/mauss/src/std/object.ts | 72 ------------ 8 files changed, 184 insertions(+), 204 deletions(-) delete mode 100644 workspace/mauss/src/std/array.spec.ts delete mode 100644 workspace/mauss/src/std/array.ts rename workspace/mauss/src/std/{object.spec.ts => index.spec.ts} (57%) rename workspace/mauss/src/std/{object.test.ts => index.test.ts} (58%) delete mode 100644 workspace/mauss/src/std/object.ts diff --git a/workspace/mauss/src/core/standard/index.spec.ts b/workspace/mauss/src/core/standard/index.spec.ts index fff8e6f7..02a92be5 100644 --- a/workspace/mauss/src/core/standard/index.spec.ts +++ b/workspace/mauss/src/core/standard/index.spec.ts @@ -69,7 +69,7 @@ suites['identical/']('identical object checks', () => { assert.ok(std.identical({ x: [{}], y: { a: 0 } }, { x: [{}], y: { a: 0 } })); }); suites['identical/']('identical clone', async () => { - const { clone } = await import('../../std/object.js'); + const { clone } = await import('../../std/index.js'); const data = { a: [1, '', {}], o: { now: new Date() } }; assert.ok(std.identical(data, clone(data))); }); diff --git a/workspace/mauss/src/std/README.md b/workspace/mauss/src/std/README.md index ae30bd84..02d10d74 100644 --- a/workspace/mauss/src/std/README.md +++ b/workspace/mauss/src/std/README.md @@ -6,6 +6,17 @@ Standard modules, augmented and refined. import { :util } from 'mauss/std'; ``` +## `augment` + +Augments the source object with various utility methods, such as + +- `build(keys: string[])` - creates a new object with the keys passed +- `readonly entries` - returns an array of the object entries +- `filter(keys: string[])` - returns the object with only the keys passed +- `freeze()` - deep freezes the object +- `readonly keys` - returns an array of the object keys +- `readonly size` - returns the size of the object + ## `clone` Original function, creates a deep copy of any data type, use sparingly. @@ -16,13 +27,9 @@ export function clone(i: T): T; Creating a copy of a data type, especially an object, is useful for removing the reference to the original object, keeping it clean from unexpected changes and side effects. This is possible because we are creating a new instance, making sure that any mutation or changes that are applied won't affect one or the other. -## `freeze` - -Augmented `Object.freeze()`, deep freezes and strongly-typed. - ## `iterate` -Original function, iterate over the key-value pair of an object, returns a new object using the pairs returned from the callback function. If callback is omitted, the default behaviour will create a deep copy of the original object. +Original function, iterate over the key-value pair of an object, returns a new object using the pairs returned from the callback function. If callback is omitted, the default behavior will create a deep copy of the original object. ```typescript export function iterate( @@ -36,26 +43,6 @@ export function iterate( The returned object will be filtered to only contain a key-value pair of the 2-tuple from `fn()`, any other values returned from the callback will be ignored, i.e. `void | Falsy`. -## `pick` - -Original function, returns a curried function that constructs a new object consisting of the properties passed to `keys` as an array of strings. - -```typescript -export function pick(keys: K): (o: T) => Pick; -``` - -In the case of picking the same properties from multiple different objects, we can store it as `unwrap` - -```typescript -const unwrap = pick(['a', 'b', 'c']); - -unwrap({ ... }); -``` - -## `size` - -Convenience method to get the size of an object by checking the `length` of its keys. - ## `zip` Original function, aggregates elements from each of the arrays and returns a single array of objects with the length of the largest array. diff --git a/workspace/mauss/src/std/array.spec.ts b/workspace/mauss/src/std/array.spec.ts deleted file mode 100644 index 3864b35f..00000000 --- a/workspace/mauss/src/std/array.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { suite } from 'uvu'; -import * as assert from 'uvu/assert'; - -import * as ntv from './array.js'; - -const suites = { - 'arr/zip': suite('arr/zip'), -}; - -suites['arr/zip']('zip multiple arrays of objects', () => { - const zipped = ntv.zip( - [{ a: 0 }, { x: 0 }], - [{ b: 0 }, { y: 0 }], - [{ c: 0 }, { z: 0 }], - [{ d: 0 }, { x: 1 }], - ); - - assert.equal(zipped, [ - { a: 0, b: 0, c: 0, d: 0 }, - { x: 1, y: 0, z: 0 }, - ]); -}); -suites['arr/zip']('zip multiple uneven arrays', () => { - const zipped = ntv.zip( - [{ a: 0 }], - [{ a: 1 }, { x: 0 }], - [{ b: 0 }, { y: 0 }], - [{ c: 0 }, { z: 0 }, { v: 0 }], - [{ d: 0 }, { x: 1 }], - [null, null, { w: 0 }, { w: 0 }], - [null, null, { x: 0 }, { x: 0 }], - [null, null, { v: 1 }, { y: 0 }], - ); - - assert.equal(zipped, [ - { a: 1, b: 0, c: 0, d: 0 }, - { x: 1, y: 0, z: 0 }, - { v: 1, w: 0, x: 0 }, - { w: 0, x: 0, y: 0 }, - ]); -}); -suites['arr/zip']('zip remove all nullish index', () => { - const zipped = ntv.zip( - [{ a: 0 }, null, { x: 0 }, null, { a: 0 }, undefined], - [{ b: 0 }, null, { y: 0 }, undefined, { b: 0 }, null], - [{ c: 0 }, null, { z: 0 }, undefined, { c: 0 }, null], - [{ d: 0 }, null, { x: 1 }, null, { d: 0 }, undefined], - ); - - assert.equal(zipped, [ - { a: 0, b: 0, c: 0, d: 0 }, - { x: 1, y: 0, z: 0 }, - { a: 0, b: 0, c: 0, d: 0 }, - ]); -}); - -Object.values(suites).forEach((v) => v.run()); diff --git a/workspace/mauss/src/std/array.ts b/workspace/mauss/src/std/array.ts deleted file mode 100644 index 45055736..00000000 --- a/workspace/mauss/src/std/array.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IndexSignature, Nullish } from '../typings/aliases.js'; - -export function zip>(...arrays: T[]) { - const max = Math.max(...arrays.map((a) => a.length)); - const items: T[number][] = []; - for (let idx = 0, empty; idx < max; idx++, empty = !0) { - const zipped: T[number] = {}; - for (const prime of arrays) { - if (!prime[idx]) continue; - empty = !Object.assign(zipped, prime[idx]); - } - if (!empty) items.push(zipped); - } - return items as Record[]; -} diff --git a/workspace/mauss/src/std/object.spec.ts b/workspace/mauss/src/std/index.spec.ts similarity index 57% rename from workspace/mauss/src/std/object.spec.ts rename to workspace/mauss/src/std/index.spec.ts index 1fc28355..9a13c61c 100644 --- a/workspace/mauss/src/std/object.spec.ts +++ b/workspace/mauss/src/std/index.spec.ts @@ -1,9 +1,11 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import * as ntv from './object.js'; +import * as std from './index.js'; const suites = { + 'arr/zip': suite('arr/zip'), + 'obj/clone': suite('obj/clone'), 'obj/entries': suite('obj/entries'), 'obj/freeze': suite('obj/freeze'), @@ -13,9 +15,56 @@ const suites = { 'obj/size': suite('obj/size'), }; +suites['arr/zip']('zip multiple arrays of objects', () => { + const zipped = std.zip( + [{ a: 0 }, { x: 0 }], + [{ b: 0 }, { y: 0 }], + [{ c: 0 }, { z: 0 }], + [{ d: 0 }, { x: 1 }], + ); + + assert.equal(zipped, [ + { a: 0, b: 0, c: 0, d: 0 }, + { x: 1, y: 0, z: 0 }, + ]); +}); +suites['arr/zip']('zip multiple uneven arrays', () => { + const zipped = std.zip( + [{ a: 0 }], + [{ a: 1 }, { x: 0 }], + [{ b: 0 }, { y: 0 }], + [{ c: 0 }, { z: 0 }, { v: 0 }], + [{ d: 0 }, { x: 1 }], + [null, null, { w: 0 }, { w: 0 }], + [null, null, { x: 0 }, { x: 0 }], + [null, null, { v: 1 }, { y: 0 }], + ); + + assert.equal(zipped, [ + { a: 1, b: 0, c: 0, d: 0 }, + { x: 1, y: 0, z: 0 }, + { v: 1, w: 0, x: 0 }, + { w: 0, x: 0, y: 0 }, + ]); +}); +suites['arr/zip']('zip remove all nullish index', () => { + const zipped = std.zip( + [{ a: 0 }, null, { x: 0 }, null, { a: 0 }, undefined], + [{ b: 0 }, null, { y: 0 }, undefined, { b: 0 }, null], + [{ c: 0 }, null, { z: 0 }, undefined, { c: 0 }, null], + [{ d: 0 }, null, { x: 1 }, null, { d: 0 }, undefined], + ); + + assert.equal(zipped, [ + { a: 0, b: 0, c: 0, d: 0 }, + { x: 1, y: 0, z: 0 }, + { a: 0, b: 0, c: 0, d: 0 }, + ]); +}); + suites['obj/clone']('clone any possible data type', () => { const base = { arr: [0, 'hi', /wut/], obj: { now: new Date() } }; - const cloned = ntv.clone(base); + const cloned = std.clone(base); assert.ok(base !== cloned); assert.ok(base.arr !== cloned.arr); @@ -28,7 +77,7 @@ suites['obj/clone']('clone any possible data type', () => { }); suites['obj/entries']('return object entries', () => { - assert.equal(ntv.entries({ hello: 'world', foo: 0, bar: { baz: 1 } }), [ + assert.equal(std.augment({ hello: 'world', foo: 0, bar: { baz: 1 } }).entries, [ ['hello', 'world'], ['foo', 0], ['bar', { baz: 1 }], @@ -36,20 +85,24 @@ suites['obj/entries']('return object entries', () => { }); suites['obj/freeze']('deep freezes nested objects', () => { - const nested = ntv.freeze({ - foo: { a: 0 }, - bar: { b: 1 }, - }); + const nested = std + .augment({ + foo: { a: 0 }, + bar: { b: 1 }, + }) + .freeze(); assert.ok(Object.isFrozen(nested)); assert.ok(Object.isFrozen(nested.foo)); assert.ok(Object.isFrozen(nested.bar)); }); suites['obj/freeze']('deep freeze ignore function', () => { - const nested = ntv.freeze({ - identity: (v: any) => v, - namespace: { a() {} }, - }); + const nested = std + .augment({ + identity: (v: any) => v, + namespace: { a() {} }, + }) + .freeze(); assert.ok(!Object.isFrozen(nested.identity)); assert.equal(nested.identity(0), 0); @@ -70,8 +123,8 @@ suites['obj/iterate']('iterate over nested objects', () => { ); assert.equal( - ntv.iterate(nested, ([month, v]) => { - const updated = ntv.iterate(v, ([currency, { income, expense }]) => { + std.iterate(nested, ([month, v]) => { + const updated = std.iterate(v, ([currency, { income, expense }]) => { return [currency, { balance: income - expense }]; }); return [month, updated]; @@ -92,12 +145,12 @@ suites['obj/iterate']('iterate over nested objects', () => { }); suites['obj/iterate']('iterate with empty/falsy return', () => { assert.equal( - ntv.iterate({}, ([]) => {}), + std.iterate({}, ([]) => {}), {}, ); assert.equal( - ntv.iterate( + std.iterate( { a: '0', b: 1, c: null, d: '3', e: undefined, f: false }, ([k, v]) => v != null && v !== false && [k, v], ), @@ -105,10 +158,10 @@ suites['obj/iterate']('iterate with empty/falsy return', () => { ); type Nested = { [P in 'a' | 'b']?: { [K in 'x' | 'y']: { foo: string } } }; - ntv.iterate({ a: { x: { foo: 'ax' } } } as Nested, ([parent, v]) => { + std.iterate({ a: { x: { foo: 'ax' } } } as Nested, ([parent, v]) => { assert.equal(parent, 'a'); v && - ntv.iterate(v, ([key, { foo }]) => { + std.iterate(v, ([key, { foo }]) => { assert.equal(key, 'x'); assert.equal(foo, 'ax'); }); @@ -116,19 +169,17 @@ suites['obj/iterate']('iterate with empty/falsy return', () => { }); suites['obj/iterate']('iterate creates deep copy', () => { const original = { x: 1, y: { z: 'foo' } }; - const copy = ntv.iterate(original); + const copy = std.iterate(original); assert.ok(original !== copy); assert.ok(original.y !== copy.y); }); suites['obj/keys']('return object keys', () => { - assert.equal(ntv.keys({ a: 0, b: 1, c: 2 }), ['a', 'b', 'c']); + assert.equal(std.augment({ a: 0, b: 1, c: 2 }).keys, ['a', 'b', 'c']); }); suites['obj/pick']('pick properties from an object', () => { - const { build, filter } = ntv.pick(['a', 'b', 'c', 'd', 'e', 'z']); - - assert.equal(build({ a: 0, c: 'b', z: null }), { + assert.equal(std.augment({ a: 0, c: 'b', z: null }).build(['a', 'b', 'c', 'd', 'e', 'z']), { a: 0, b: null, c: 'b', @@ -137,15 +188,14 @@ suites['obj/pick']('pick properties from an object', () => { z: null, }); - assert.equal(filter({ a: 0, c: 'b', y: undefined, z: null }), { - a: 0, - c: 'b', - z: null, - }); + assert.equal( + std.augment({ a: 0, c: 'b', y: undefined, z: null }).filter(['a', 'b', 'c', 'd', 'e', 'z']), + { a: 0, c: 'b', z: null }, + ); }); suites['obj/size']('return size of an object', () => { - assert.equal(ntv.size({ a: 0, b: 1, c: 2 }), 3); + assert.equal(std.augment({ a: 0, b: 1, c: 2 }).size, 3); }); Object.values(suites).forEach((v) => v.run()); diff --git a/workspace/mauss/src/std/object.test.ts b/workspace/mauss/src/std/index.test.ts similarity index 58% rename from workspace/mauss/src/std/object.test.ts rename to workspace/mauss/src/std/index.test.ts index fda2c92f..8b61ec7d 100644 --- a/workspace/mauss/src/std/object.test.ts +++ b/workspace/mauss/src/std/index.test.ts @@ -1,10 +1,10 @@ -import { entries } from './object.js'; +import { augment } from './index.js'; -entries<{}>({}); +augment<{}>({}); // ---- errors ---- // @ts-expect-error - error on empty argument -entries(); +augment(); // @ts-expect-error - error on non-object type -entries(null); +augment(null); diff --git a/workspace/mauss/src/std/index.ts b/workspace/mauss/src/std/index.ts index 4a38dd56..e6d161d0 100644 --- a/workspace/mauss/src/std/index.ts +++ b/workspace/mauss/src/std/index.ts @@ -1,2 +1,89 @@ -export { zip } from './array.js'; -export { clone, entries, freeze, iterate, keys, pick, size } from './object.js'; +import type { Falsy, IndexSignature, Nullish } from '../typings/aliases.js'; +import type { AnyFunction, Entries, Freeze } from '../typings/helpers.js'; +import type { Narrow } from '../typings/prototypes.js'; + +export function augment(object: O) { + const o = clone(object); + + return { + build(keys: Narrow) { + return [...keys].reduce( + // @ts-expect-error - unknown until `keys` are passed + (a, k) => ({ ...a, [k]: k in o ? o[k] : null }), + {} as { [K in Keys[number]]: K extends keyof O ? O[K] : null }, + ); + }, + + get entries() { + return Object.entries(o) as Entries; + }, + + filter( + keys: Narrow, + ): // @ts-expect-error - 100% TS bug not mine + Pick { + const props = new Set(keys); + // @ts-expect-error - unknown until `keys` are passed + return iterate(o, ([k, v]) => props.has(k) && [k, v]); + }, + + freeze(): Freeze { + for (const key of Object.getOwnPropertyNames(o)) { + const value = o[key as keyof typeof o]; + if (value == null || typeof value !== 'object') continue; + o[key as keyof typeof o] = augment(value).freeze() as typeof value; + } + return Object.freeze(o); + }, + + get keys() { + return Object.keys(o) as Array; + }, + + get size() { + return this.keys.length; + }, + }; +} + +export function clone(i: T): T { + if (!i || typeof i !== 'object') return i; + if (Array.isArray(i)) return i.map(clone) as T; + const type = Object.prototype.toString.call(i); + if (type !== '[object Object]') return i; + return iterate(i) as T; +} + +/** + * Iterate over the key-value pair of an object, returns a new object using the pairs returned from the callback function. If callback is omitted, the default behavior will create a deep copy of the original object. + */ +export function iterate( + object: T, + callback: AnyFunction< + [entry: Entries[number], index: number], + void | Falsy | [IndexSignature, I] + > = ([k, v]) => [k, clone(v) as I], +): I extends T[keyof T] ? T : unknown { + const pairs = Object.entries(object) as Entries; + const memo: typeof pairs = []; + for (let i = 0; i < pairs.length; i++) { + const res = callback(pairs[i], i); + if (!res || res.length !== 2) continue; + memo.push(res as (typeof memo)[number]); + } + return Object.fromEntries(memo) as any; +} + +export function zip>(...arrays: T[]) { + const max = Math.max(...arrays.map((a) => a.length)); + const items: T[number][] = []; + for (let idx = 0, empty; idx < max; idx++, empty = !0) { + const zipped: T[number] = {}; + for (const prime of arrays) { + if (!prime[idx]) continue; + empty = !Object.assign(zipped, prime[idx]); + } + if (!empty) items.push(zipped); + } + return items as Record[]; +} diff --git a/workspace/mauss/src/std/object.ts b/workspace/mauss/src/std/object.ts deleted file mode 100644 index 4548ead2..00000000 --- a/workspace/mauss/src/std/object.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Falsy, IndexSignature } from '../typings/aliases.js'; -import type { AnyFunction, Entries, Freeze } from '../typings/helpers.js'; -import type { Narrow } from '../typings/prototypes.js'; - -export function clone(i: T): T { - if (!i || typeof i !== 'object') return i; - if (Array.isArray(i)) return i.map(clone) as T; - const type = Object.prototype.toString.call(i); - if (type !== '[object Object]') return i; - return iterate(i) as T; -} - -export function entries(o: T) { - return Object.entries(o) as Entries; -} - -export function freeze(o: T): Freeze { - for (const key of Object.getOwnPropertyNames(o)) { - const value = o[key as keyof typeof o]; - if (value == null || typeof value !== 'object') continue; - o[key as keyof typeof o] = freeze(value) as typeof value; - } - return Object.freeze(o); -} - -/** - * Iterate over the key-value pair of an object, returns a new object using the pairs returned from the callback function. If callback is omitted, the default behaviour will create a deep copy of the original object. - */ -export function iterate( - object: T, - callback: AnyFunction< - [entry: Entries[number], index: number], - void | Falsy | [IndexSignature, I] - > = ([k, v]) => [k, clone(v) as I], -): I extends T[keyof T] ? T : unknown { - const pairs = entries(object); - const memo: typeof pairs = []; - for (let i = 0; i < pairs.length; i++) { - const res = callback(pairs[i], i); - if (!res || res.length !== 2) continue; - memo.push(res as (typeof memo)[number]); - } - return Object.fromEntries(memo) as any; -} - -export function keys(o: T) { - return Object.keys(o) as Array; -} - -export function pick(keys: Narrow) { - const props = new Set(keys); - - return { - build(o: T, initial = null) { - return [...props].reduce( - // @ts-expect-error - unknown until `keys` are passed - (a, k) => ({ ...a, [k]: k in o ? o[k] : initial }), - // @ts-expect-error - too hard for TS - {} as { [K in Keys[number]]: Pick extends {} ? typeof initial : T[K] }, - ); - }, - // @ts-expect-error - 100% TS bug not mine - filter(o: T): Pick { - // @ts-expect-error - unknown until `keys` are passed - return iterate(o, ([k, v]) => props.has(k) && [k, v]); - }, - }; -} - -export function size(o: T): number { - return Object.keys(o).length; -}