From ede6a670c3e91d0612c23f275de35ff5269d1833 Mon Sep 17 00:00:00 2001 From: drizzer14 Date: Wed, 26 Jun 2024 16:33:30 +0300 Subject: [PATCH] feat: create lens module BREAKING CHANGE: get and set functions were moved into lens module --- src/index.ts | 2 - src/{ => lens}/get.ts | 16 ++++-- src/lens/index.ts | 2 + src/lens/set.ts | 117 +++++++++++++++++++++++++++++++++++++++++ tests/get.spec.ts | 2 +- tests/guard.spec.ts | 14 ++--- tests/set.spec.ts | 33 ++++++++++++ typetests/get.test.ts | 2 +- typetests/pipe.test.ts | 2 +- typetests/set.test.ts | 84 +++++++++++++++++++++++++++++ 10 files changed, 257 insertions(+), 17 deletions(-) rename src/{ => lens}/get.ts (82%) create mode 100644 src/lens/index.ts create mode 100644 src/lens/set.ts create mode 100644 tests/set.spec.ts create mode 100644 typetests/set.test.ts diff --git a/src/index.ts b/src/index.ts index 3fa8ebd..a34b53a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ -export { default as get } from './get' -export { default as set } from './set' export { default as tap } from './tap' export { default as pipe } from './pipe' export { default as apply } from './apply' diff --git a/src/get.ts b/src/lens/get.ts similarity index 82% rename from src/get.ts rename to src/lens/get.ts index c7d132e..6aa62ef 100644 --- a/src/get.ts +++ b/src/lens/get.ts @@ -1,9 +1,9 @@ /** - * @module Get + * @module Lens */ -import permutation2 from './permutation/permutation-2' -import type { Flatten, Flattenable } from './types/flatten' +import permutation2 from '../permutation/permutation-2' +import type { Flatten, Flattenable } from '../types/flatten' /** * Gets the value type inside a nested object type `Source` by provided `Path` @@ -16,6 +16,7 @@ export type Get< Source extends Record ? Path extends `${number}.${infer Right}` ? Get + // @ts-ignore : Path extends `${infer Left extends keyof Source}.${infer Right}` // @ts-ignore ? Get, Right> | Extract @@ -23,8 +24,8 @@ export type Get< ? Source[Path] : never : Source extends any[] - ? Path extends `${infer Left extends number}.${infer Right}` - ? Get, Right> | Extract + ? Path extends `${infer Index extends number}.${infer Right}` + ? Get, Right> | Extract : Path extends `${infer Index extends number}` ? Source[Index] : never @@ -34,23 +35,27 @@ export type Get< * Gets the value inside a nested `source` object by provided `path` * written in dot-notation. */ +// @ts-ignore export default function get< Source extends Flattenable, Path extends Flatten > ( path: Path + // @ts-ignore ): (source: Source) => Get /** * Gets the value inside a nested `source` object by provided `path` * written in dot-notation. */ +// @ts-ignore export default function get< Source extends Flattenable, Path extends Flatten > ( source: Source, path: Path + // @ts-ignore ): Get /** @@ -58,6 +63,7 @@ export default function get< * written in dot-notation. */ export default function get (...args: [any, any?]): any { + // @ts-ignore return permutation2( < Source extends Flattenable, diff --git a/src/lens/index.ts b/src/lens/index.ts new file mode 100644 index 0000000..8d1843e --- /dev/null +++ b/src/lens/index.ts @@ -0,0 +1,2 @@ +export { default as set } from './set' +export { default as get } from './get' diff --git a/src/lens/set.ts b/src/lens/set.ts new file mode 100644 index 0000000..4bed34b --- /dev/null +++ b/src/lens/set.ts @@ -0,0 +1,117 @@ +/** + * @module Lens + */ + +import type { Unshift } from '../types/unshift' +import permutation3 from '../permutation/permutation-3' +import type { Flatten, Flattenable } from '../types/flatten' + +/** + * Sets the `Value` type inside a nested object type `Source` by provided `Path` + * written in dot-notation. + */ +export type Set< + Source extends Flattenable, + Path extends string, + Value +> = + Source extends Record + ? Path extends `${number}.${infer Right}` + ? Set + // @ts-ignore + : Path extends `${infer Left extends keyof Source}.${infer Right}` + ? { + [Key in keyof Source]: Key extends Exclude + ? Source[Key] + // @ts-ignore + : Set, Right, Value> + } + : Path extends keyof Source + ? { + [Key in keyof Source]: Key extends Exclude + ? Source[Key] + : Value + } + : never + : Source extends any[] + ? Path extends `${infer Index extends number}.${infer Right}` + ? Unshift, Right, Value>> + : Path extends `${infer Index extends number}` + ? Unshift + : never + : never + +/** + * Sets the `value` inside a nested `source` object by provided `path` + * written in dot-notation. + */ +// @ts-ignore +export default function set< + Source extends Flattenable, + Path extends Flatten, + Value +> ( + path: Path, + value: Value, + // @ts-ignore +): (source: Source) => Set + +/** + * Sets the `value` inside a nested `source` object by provided `path` + * written in dot-notation. + */ +// @ts-ignore +export default function set< + Source extends Flattenable, + Path extends Flatten, + Value +> ( + source: Source, + path: Path, + value: Value + // @ts-ignore +): Set + +/** + * Sets the `value` inside a nested `source` object by provided `path` + * written in dot-notation. + */ +export default function set (...args: [any, any, any?]): any { + // @ts-ignore + return permutation3( + // @ts-ignore + < + Source extends Flattenable, + Path extends Flatten, + Value + >( + source: Source, + path: Path, + value: Value + // @ts-ignore + ): Set => { + // @ts-ignore + const keys = path.split('.') + const length = keys.length + + let result = structuredClone(source) + + if (length === 0) { + result[path as keyof Source] = value as Source[keyof Source] + } + + let scope: any = result[keys[0]! as keyof Source] + + for (let index = 1; index < length; index += 1) { + if (index === length - 1) { + scope[keys?.[index] as keyof typeof scope] = value + } else { + scope = scope?.[keys?.[index] as keyof typeof scope] + } + } + + // @ts-ignore + return result as Set + } + )(...args) +} diff --git a/tests/get.spec.ts b/tests/get.spec.ts index 3ef2d97..757e7b8 100644 --- a/tests/get.spec.ts +++ b/tests/get.spec.ts @@ -1,4 +1,4 @@ -import sut from '../src/get' +import sut from '../src/lens/get' describe('get', () => { describe('when accessing a property', () => { diff --git a/tests/guard.spec.ts b/tests/guard.spec.ts index cdd03b2..bd1f003 100644 --- a/tests/guard.spec.ts +++ b/tests/guard.spec.ts @@ -4,8 +4,8 @@ describe('guard', () => { describe('when encountering a truthy guard', () => { it('should execute the function next to that guard', () => { expect( - sut<(x: number) => number>( - [(x) => x < 5, (x) => x + 1], + sut( + [(x) => x < 5, (x: number) => x + 1], [(x) => x === 5, (x) => x - 1], () => 1 )(5) @@ -15,21 +15,21 @@ describe('guard', () => { it('should not check further guards', () => { const guardNotToBeCalled = jest.fn((x: number) => x < 5) - sut<(x: number) => number>( - [(x) => x === 5, (x) => x - 1], + sut( + [(x) => x === 5, (x: number) => x - 1], [guardNotToBeCalled, (x) => x + 1], () => 1 )(5) - expect(guardNotToBeCalled).not.toBeCalled() + expect(guardNotToBeCalled).not.toHaveBeenCalled() }) }) describe('when no truthy guards are present', () => { it('should execute the default function', () => { expect( - sut<(x: number) => number>( - [(x) => x < 5, (x) => x + 1], + sut( + [(x) => x < 5, (x: number) => x + 1], [(x) => x > 5, (x) => x - 1], () => 1 )(5) diff --git a/tests/set.spec.ts b/tests/set.spec.ts new file mode 100644 index 0000000..ed1806a --- /dev/null +++ b/tests/set.spec.ts @@ -0,0 +1,33 @@ +import sut from '../src/lens/set' + +describe('set', () => { + describe('when setting a property', () => { + it('should return source copy with modified value', () => { + const source = { a: { b: { c: 1 } } } + const result = sut(source, 'a.b.c', 2) + + expect(result).toEqual({ a: { b: { c: 2 } } }) + expect(source === result).toBe(false) + }) + }) + + describe('when setting a nested array\'s element', () => { + it('should return source copy with modified value', () => { + const source = { a: { b: { c: [1] } } } + const result = sut(source, 'a.b.c.0', 2) + + expect(result).toEqual({ a: { b: { c: [2] } } }) + expect(source === result).toBe(false) + }) + + describe('when an element is object', () => { + it('should return source copy with modified value', () => { + const source = { a: { b: { c: [{ a: { b: 2 } }] } } } + const result = sut(source, 'a.b.c.0.a.b', 3) + + expect(result).toEqual({ a: { b: { c: [{ a: { b: 3 } }] } } }) + expect(source === result).toBe(false) + }) + }) + }) +}) diff --git a/typetests/get.test.ts b/typetests/get.test.ts index c07a683..902a5cd 100644 --- a/typetests/get.test.ts +++ b/typetests/get.test.ts @@ -1,4 +1,4 @@ -import get from '../src/get' +import get from '../src/lens/get' import tacit from './tacit' diff --git a/typetests/pipe.test.ts b/typetests/pipe.test.ts index a68d163..550f47b 100644 --- a/typetests/pipe.test.ts +++ b/typetests/pipe.test.ts @@ -72,7 +72,7 @@ const a8: Maybe = tacit( (value) => `${value}` === '1', ), pipe( - identity, + (value) => Number(value), (value: number): string => `${value}`, ), ), diff --git a/typetests/set.test.ts b/typetests/set.test.ts new file mode 100644 index 0000000..a568895 --- /dev/null +++ b/typetests/set.test.ts @@ -0,0 +1,84 @@ +import set from '../src/lens/set' + +import tacit from './tacit' + +const source = { + a: 1, + b: { + c: true, + d: { + e: [ + { + f: '' + } + ], + g: [ + 3 + ] + } + } +} + +// region full + +const a1: typeof source = set( + source, + 'a', + 2 +) + +const a2: typeof source = set( + source, + 'b.c', + false +) + +const a3: typeof source = set( + source, + 'b.d.e.0.f', + 'f' +) + +const a4: typeof source = set( + source, + 'b.d.g', + [2] +) + +// endregion + +// region tacit + +const b1: typeof source = tacit( + set( + 'a', + 2 + ), + source +) + +const b2: typeof source = tacit( + set( + 'b.c', + false + ), + source +) + +const b3: typeof source = tacit( + set( + 'b.d.e.0.f', + 'f' + ), + source +) + +const b4: typeof source = tacit( + set( + 'b.d.g', + [2] + ), + source +) + +// endregion