From 87b7a09a3279ee3a3be1c1c54ba57ac4c0cc0cf6 Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Thu, 25 May 2023 21:11:18 -0400 Subject: [PATCH] feat(utils): `get` Signed-off-by: Lexus Drumgold --- __fixtures__/person.interface.ts | 41 +++++++++++++++++ src/types/get.ts | 19 +++++--- src/utils/__tests__/get.spec-d.ts | 22 +++++++++ src/utils/__tests__/get.spec.ts | 62 ++++++++++++++++++++++++++ src/utils/get.ts | 74 +++++++++++++++++++++++++++++++ src/utils/index.ts | 1 + 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 __fixtures__/person.interface.ts create mode 100644 src/utils/__tests__/get.spec-d.ts create mode 100644 src/utils/__tests__/get.spec.ts create mode 100644 src/utils/get.ts diff --git a/__fixtures__/person.interface.ts b/__fixtures__/person.interface.ts new file mode 100644 index 00000000..da0cf725 --- /dev/null +++ b/__fixtures__/person.interface.ts @@ -0,0 +1,41 @@ +/** + * @file Test Fixtures - Person + * @module tests/fixtures/Person + */ + +/** + * Object representing a person. + */ +interface Person { + /** + * Person's age. + */ + age: number + + /** + * Person's friends. + */ + friends?: Person[] + + /** + * Object representing a person's name. + */ + name: { + /** + * First name. + */ + first: string + + /** + * Last name. + */ + last: string + + /** + * Middle name. + */ + middle?: string + } +} + +export type { Person as default } diff --git a/src/types/get.ts b/src/types/get.ts index 3050c80c..099a0b0a 100644 --- a/src/types/get.ts +++ b/src/types/get.ts @@ -9,6 +9,7 @@ import type EmptyObject from './empty-object' import type EmptyString from './empty-string' import type Fallback from './fallback' import type IfEqual from './if-equal' +import type IfUndefined from './if-undefined' import type IndexSignature from './index-signature' import type NumberString from './number-string' import type Numeric from './numeric' @@ -32,14 +33,18 @@ type Get = T extends : K extends `${infer H extends NumberString}.${infer Tail}` ? NonNullable extends string | readonly unknown[] ? H extends Numeric | number - ? undefined extends At, H> - ? F - : IfEqual< - At, H>, - NonNullable, H>>, - Get[H & keyof NonNullable], Tail, F>, - F | Get[H & keyof NonNullable], Tail, F> + ? At, H> extends infer U + ? IfUndefined< + U, + F, + IfEqual< + U, + NonNullable, + Get[H & keyof NonNullable], Tail, F>, + F | Get[H & keyof NonNullable], Tail, F> + > > + : never : never : H extends keyof NonNullable ? IfEqual< diff --git a/src/utils/__tests__/get.spec-d.ts b/src/utils/__tests__/get.spec-d.ts new file mode 100644 index 00000000..c27b26ed --- /dev/null +++ b/src/utils/__tests__/get.spec-d.ts @@ -0,0 +1,22 @@ +/** + * @file Type Tests - get + * @module tutils/utils/tests/unit-d/get + */ + +import type Person from '#fixtures/person.interface' +import type { EmptyString, Get } from '#src/types' +import type testSubject from '../get' + +describe('unit-d:utils/get', () => { + it('should return Get', () => { + // Arrange + type T = Person + type P = 'friends.0.name.middle' + type F = EmptyString + + // Expect + expectTypeOf>().returns.toEqualTypeOf< + Get + >() + }) +}) diff --git a/src/utils/__tests__/get.spec.ts b/src/utils/__tests__/get.spec.ts new file mode 100644 index 00000000..925350fe --- /dev/null +++ b/src/utils/__tests__/get.spec.ts @@ -0,0 +1,62 @@ +/** + * @file Unit Tests - get + * @module tutils/utils/tests/unit/get + */ + +import type Person from '#fixtures/person.interface' +import type { EmptyString } from '#src/types' +import testSubject from '../get' + +describe('unit:utils/get', () => { + let person: Person + + beforeAll(() => { + person = { + age: faker.number.int({ max: 25, min: 18 }), + friends: [ + { + age: faker.number.int({ max: 25, min: 18 }), + name: { + first: faker.person.firstName(), + last: faker.person.lastName() + } + } + ], + name: { + first: faker.person.firstName(), + last: faker.person.lastName(), + middle: faker.person.middleName() + } + } + }) + + it('should return fallback if indexed value is undefined', () => { + // Arrange + const fallback: EmptyString = '' + + // Act + const result = testSubject(person, 'friends.0.name.middle', fallback) + + // Expect + expect(result).to.deep.equal(fallback) + }) + + it('should return dynamically indexed value', () => { + // Arrange + const cases: [...Parameters, unknown][] = [ + ['person', 0, undefined, 'p'], + [null, 'age', undefined, null], + [person, 'age', undefined, person.age], + [person, 'age.', undefined, person.age], + [person, 'friends', undefined, person.friends], + [person, 'friends.0.name.last', undefined, person.friends![0]!.name.last], + [person, 'name.first', undefined, person.name.first], + [person, 'name.first.-1', undefined, person.name.first.at(-1)] + ] + + // Act + Expect + cases.forEach(([value, path, fallback, expected]) => { + expect(testSubject(value, path, fallback)).to.deep.equal(expected) + }) + }) +}) diff --git a/src/utils/get.ts b/src/utils/get.ts new file mode 100644 index 00000000..f03c40e2 --- /dev/null +++ b/src/utils/get.ts @@ -0,0 +1,74 @@ +/** + * @file Utilities - get + * @module tutils/utils/get + */ + +import type { Get, NumberString } from '#src/types' +import at from './at' +import cast from './cast' +import isArray from './is-array' +import isEmptyString from './is-empty-string' +import isNIL from './is-nil' +import isNumeric from './is-numeric' +import isString from './is-string' +import isUndefined from './is-undefined' +import trim from './trim' + +/** + * Dynamically indexes `data` at `path`. + * + * If the indexed value is `undefined`, `fallback` will be returned instead. + * + * Supports dot-notation for nested paths and array indexing. + * + * @template T - Value to index + * @template P - Index path + * @template F - Fallback value type + * + * @param {T} data - Value to index + * @param {P} path - Index path + * @param {F} [fallback] - Fallback value + * @return {Get} Dynamically indexed value or `fallback` + */ +function get( + data: T, + path: P, + fallback?: F +): Get { + /** + * Path segments. + * + * @const {string[]} segments + */ + const segments: string[] = `${path}`.split(/[.[\]]/g).map(trim) + + /** + * Dynamically indexed value. + * + * @var {unknown} value + */ + let value: unknown = data + + // dynamically index data + for (const key of segments) { + // exit early if indexed value is null or undefined + if (isNIL(value)) break + + // do nothing if key is an empty string + if (isEmptyString(key)) continue + + // reset indexed value + switch (true) { + case (isArray(value) || isString(value)) && isNumeric(key): + value = at(cast(value), +key) + break + default: + value = cast>(value)[key] + break + } + } + + return (isUndefined(value) ? fallback : value) as Get +} + +export default get diff --git a/src/utils/index.ts b/src/utils/index.ts index f08b326b..23c23eb8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,6 +14,7 @@ export { default as count } from './count' export { default as diff } from './diff' export { default as equal } from './equal' export { default as fork } from './fork' +export { default as get } from './get' export { default as group } from './group' export { default as includes } from './includes' export { default as intersection } from './intersection'