Skip to content

Commit

Permalink
feat: create lens module
Browse files Browse the repository at this point in the history
BREAKING CHANGE: get and set functions were moved into lens module
  • Loading branch information
drizzer14 committed Jun 26, 2024
1 parent 341393b commit ede6a67
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 17 deletions.
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
16 changes: 11 additions & 5 deletions src/get.ts → src/lens/get.ts
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -16,15 +16,16 @@ export type Get<
Source extends Record<string, unknown>
? Path extends `${number}.${infer Right}`
? Get<Source, Right>
// @ts-ignore
: Path extends `${infer Left extends keyof Source}.${infer Right}`
// @ts-ignore
? Get<Exclude<Source[Left], undefined>, Right> | Extract<Source[Left], undefined>
: Path extends keyof Source
? Source[Path]
: never
: Source extends any[]
? Path extends `${infer Left extends number}.${infer Right}`
? Get<Exclude<Source[Left], undefined>, Right> | Extract<Source[Left], undefined>
? Path extends `${infer Index extends number}.${infer Right}`
? Get<Exclude<Source[Index], undefined>, Right> | Extract<Source[Index], undefined>
: Path extends `${infer Index extends number}`
? Source[Index]
: never
Expand All @@ -34,30 +35,35 @@ 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<Source>
> (
path: Path
// @ts-ignore
): (source: Source) => Get<Source, Path>

/**
* 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: Source,
path: Path
// @ts-ignore
): Get<Source, Path>

/**
* Gets the value inside a nested `source` object by provided `path`
* written in dot-notation.
*/
export default function get (...args: [any, any?]): any {
// @ts-ignore
return permutation2(
<
Source extends Flattenable,
Expand Down
2 changes: 2 additions & 0 deletions src/lens/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as set } from './set'
export { default as get } from './get'
117 changes: 117 additions & 0 deletions src/lens/set.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
? Path extends `${number}.${infer Right}`
? Set<Source, Right, Value>
// @ts-ignore
: Path extends `${infer Left extends keyof Source}.${infer Right}`
? {
[Key in keyof Source]: Key extends Exclude<keyof Source, Left>
? Source[Key]
// @ts-ignore
: Set<Exclude<Source[Left], undefined>, Right, Value>
}
: Path extends keyof Source
? {
[Key in keyof Source]: Key extends Exclude<keyof Source, Path>
? Source[Key]
: Value
}
: never
: Source extends any[]
? Path extends `${infer Index extends number}.${infer Right}`
? Unshift<Source, Index, Set<Exclude<Source[Index], undefined>, Right, Value>>
: Path extends `${infer Index extends number}`
? Unshift<Source, Index, Value>
: 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<Source>,
Value
> (
path: Path,
value: Value,
// @ts-ignore
): (source: Source) => Set<Source, Path, Value>

/**
* 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<Source>,
Value
> (
source: Source,
path: Path,
value: Value
// @ts-ignore
): Set<Source, Path, Value>

/**
* 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<Source>,
Value
>(
source: Source,
path: Path,
value: Value
// @ts-ignore
): Set<Source, Path, Value> => {
// @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<Source, Path, Value>
}
)(...args)
}
2 changes: 1 addition & 1 deletion tests/get.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sut from '../src/get'
import sut from '../src/lens/get'

describe('get', () => {
describe('when accessing a property', () => {
Expand Down
14 changes: 7 additions & 7 deletions tests/guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions tests/set.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
2 changes: 1 addition & 1 deletion typetests/get.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import get from '../src/get'
import get from '../src/lens/get'

import tacit from './tacit'

Expand Down
2 changes: 1 addition & 1 deletion typetests/pipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const a8: Maybe<boolean> = tacit(
(value) => `${value}` === '1',
),
pipe(
identity,
(value) => Number(value),
(value: number): string => `${value}`,
),
),
Expand Down
84 changes: 84 additions & 0 deletions typetests/set.test.ts
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ede6a67

Please sign in to comment.