Skip to content

Commit

Permalink
feat(utils): define, descriptor, enumerable, hasOwn
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 20, 2023
1 parent 3866ad0 commit b22f86f
Show file tree
Hide file tree
Showing 16 changed files with 440 additions and 6 deletions.
69 changes: 69 additions & 0 deletions __tests__/setup/matchers/descriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @file Custom Matchers - descriptor
* @module tests/setup/matchers/descriptor
*/

import type { PropertyDescriptor } from '#src/interfaces'
import type { Optional } from '#src/types'
import type { MatcherState, SyncExpectationResult } from '@vitest/expect'
import { dequal } from 'dequal'

/**
* Asserts `target` has its own property descriptor for the given `key`.
*
* Enumerable and non-enumerable properties are included in the search.
*
* @template T - Property value type
*
* @this {MatcherState}
*
* @param {unknown} target - Value to look for `key` in
* @param {string | symbol} key - Property to check
* @param {PropertyDescriptor<T>} descriptor - Expected property descriptor
* @return {SyncExpectationResult} Expectation result
*/
function descriptor<T = any>(
this: MatcherState,
target: unknown,
key: string | symbol,
descriptor: PropertyDescriptor<T>
): SyncExpectationResult {
/**
* Own property descriptor for {@linkcode key}.
*
* @const {Optional<PropertyDescriptor>} actual
*/
const actual: Optional<PropertyDescriptor> = Object.getOwnPropertyDescriptor(
target,
key
)

return {
actual,
expected: descriptor,
message: (): string => {
/**
* Stringified target.
*
* @const {string} received
*/
const received: string = this.utils.printReceived(target)

/**
* Stringified property descriptor object.
*
* @const {string} expected
*/
const expected: string = this.utils.printExpected(descriptor)

return [
`expected ${received} to have property descriptor`,
`for "${key.toString()}"`,
actual ? `${this.isNot ? 'not ' : ''}deeply equal ${expected}` : ''
].join(' ')
},
pass: dequal(actual, descriptor)
}
}

expect.extend({ descriptor })
2 changes: 1 addition & 1 deletion __tests__/setup/matchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
* @see https://vitest.dev/guide/extending-matchers.html
*/

export {}
import './descriptor'
19 changes: 19 additions & 0 deletions src/utils/__tests__/define.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @file Type Tests - define
* @module tutils/utils/tests/unit-d/define
*/

import type Vehicle from '#fixtures/types/vehicle'
import type { Opaque } from '#src/types'
import type testSubject from '../define'

describe('unit-d:utils/define', () => {
it('should return U', () => {
// Arrange
type T = Vehicle
type U = Opaque<Vehicle, 'vehicle'>

// Expect
expectTypeOf<typeof testSubject<T, string, U>>().returns.toEqualTypeOf<U>()
})
})
61 changes: 61 additions & 0 deletions src/utils/__tests__/define.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @file Unit Tests - define
* @module tutils/utils/tests/unit/define
*/

import VEHICLE from '#fixtures/vehicle'
import type { PropertyDescriptor } from '#src/interfaces'
import testSubject from '../define'
import description from '../descriptor'

describe('unit:utils/define', () => {
describe('add property', () => {
let property: string
let vrm: string

beforeAll(() => {
property = 'vrm'
vrm = faker.vehicle.vrm()
})

it('should return obj with new property', () => {
// Act
const result = testSubject(VEHICLE, property, { value: vrm })

// Expect
expect(result).to.have.descriptor(property, {
configurable: true,
enumerable: true,
value: vrm,
writable: true
})
})
})

describe('modify property', () => {
let property: string
let vin: string

beforeAll(() => {
property = 'vin'
vin = faker.vehicle.vin()
})

it('should return obj with modified property', () => {
// Arrange
const descriptor: PropertyDescriptor<typeof vin> = {
enumerable: false,
value: vin
}

// Act
const result = testSubject(VEHICLE, property, descriptor)

// Expect
expect(result).to.have.descriptor(property, {
...description(VEHICLE, property),
...descriptor
})
})
})
})
18 changes: 18 additions & 0 deletions src/utils/__tests__/descriptor.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @file Type Tests - descriptor
* @module tutils/utils/tests/unit-d/descriptor
*/

import type { PropertyDescriptor } from '#src/interfaces'
import type testSubject from '../descriptor'

describe('unit-d:utils/descriptor', () => {
it('should return PropertyDescriptor<T>', () => {
// Arrange
type T = number
type Expect = PropertyDescriptor<T>

// Expect
expectTypeOf<typeof testSubject<T>>().returns.toEqualTypeOf<Expect>()
})
})
32 changes: 32 additions & 0 deletions src/utils/__tests__/descriptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @file Unit Tests - descriptor
* @module tutils/utils/tests/unit/descriptor
*/

import VEHICLE from '#fixtures/vehicle'
import type { PropertyDescriptor } from '#src/interfaces'
import type { PropertyKey } from '#src/types'
import testSubject from '../descriptor'

describe('unit:utils/descriptor', () => {
it('should return PropertyDescriptor object', () => {
// Arrange
const cases: [PropertyKey, PropertyDescriptor][] = [
['driver', {}],
[
'vin',
{
configurable: true,
enumerable: true,
value: VEHICLE.vin,
writable: true
}
]
]

// Act + Expect
cases.forEach(([key, expected]) => {
expect(testSubject(VEHICLE, key)).to.eql(expected)
})
})
})
35 changes: 35 additions & 0 deletions src/utils/__tests__/enumerable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @file Unit Tests - enumerable
* @module tutils/utils/tests/unit/enumerable
*/

import type Vehicle from '#fixtures/types/vehicle'
import VEHICLE from '#fixtures/vehicle'
import type { PropertyKey } from '#src/types'
import define from '../define'
import descriptor from '../descriptor'
import testSubject from '../enumerable'

describe('unit:utils/enumerable', () => {
let value: Vehicle

beforeAll(() => {
value = define({ ...VEHICLE }, 'vrm', {
...descriptor(VEHICLE, 'vin'),
enumerable: false,
value: faker.vehicle.vrm()
})
})

it('should equal false if key is not enumerable own property', () => {
// Arrange
const cases: PropertyKey[] = ['drivers', 'vrm']

// Act + Expect
cases.forEach(key => expect(testSubject(value, key)).to.be.false)
})

it('should equal true if key is enumerable own property', () => {
expect(testSubject(value, 'vin')).to.be.true
})
})
24 changes: 24 additions & 0 deletions src/utils/__tests__/has-own.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @file Unit Tests - hasOwn
* @module tutils/utils/tests/unit/hasOwn
*/

import testSubject from '../has-own'

describe('unit:utils/hasOwn', () => {
let obj: { hello: string }
let tag: symbol

beforeAll(() => {
tag = Symbol('obj')
obj = { hello: 'world', [tag]: tag.description }
})

it('should equal false if key is not own property', () => {
expect(testSubject(obj, obj.hello)).to.be.false
})

it('should equal true if key is own property', () => {
;[tag, 'hello'].forEach(key => expect(testSubject(obj, key)).to.be.true)
})
})
8 changes: 3 additions & 5 deletions src/utils/cast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
/**
* Casts `value`.
*
* @template R - Return value type
* @template T - Return value type
*
* @param {unknown} value - Value to cast
* @return {R} `value`
* @return {T} `value`
*/
function cast<R>(value: unknown): R {
return value as R
}
const cast = <T>(value: unknown): T => value as T

export default cast
48 changes: 48 additions & 0 deletions src/utils/define.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @file Utilities - define
* @module tutils/utils/define
*/

import type { PropertyDescriptor } from '#src/interfaces'
import type { PropertyKey } from '#src/types'
import cast from './cast'
import description from './descriptor'
import hasOwn from './has-own'

/**
* Adds a property to an object, or modifies attributes of an existing property.
*
* @see {@linkcode PropertyDescriptor}
*
* @todo examples
*
* @template T - Object on which to add or modify property
* @template V - Property value
* @template U - Updated object type
*
* @param {T} obj - Object on which to add or modify the property
* @param {PropertyKey} property - Property to add or modify
* @param {PropertyDescriptor<V>} [descriptor] - Property descriptor object
* @return {U} Updated object
*/
const define = <T extends object, V = any, U = T>(
obj: T,
property: PropertyKey,
descriptor: PropertyDescriptor<V> = {}
): U => {
/**
* Default attributes.
*
* @const {PropertyDescriptor<V>} defaults
*/
const defaults: PropertyDescriptor<V> = hasOwn(obj, property)
? description(obj, property)
: { configurable: true, enumerable: true, writable: true }

return Object.defineProperty(cast(obj), property, {
...defaults,
...descriptor
})
}

export default define
27 changes: 27 additions & 0 deletions src/utils/descriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @file Utilities - descriptor
* @module tutils/utils/descriptor
*/

import type { PropertyDescriptor } from '#src/interfaces'
import type { PropertyKey } from '#src/types'

/**
* Returns an object describing the configuration of a specific property.
*
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
*
* @todo examples
*
* @template T - Property value type
*
* @param {unknown} target - Value to look for `key` in
* @param {PropertyKey} key - Property key
* @return {PropertyDescriptor<T>} Property descriptor object
*/
const descriptor = <T = any>(
target: unknown,
key: PropertyKey
): PropertyDescriptor<T> => Object.getOwnPropertyDescriptor(target, key) ?? {}

export default descriptor
26 changes: 26 additions & 0 deletions src/utils/enumerable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @file Utilities - enumerable
* @module tutils/utils/enumerable
*/

import type { PropertyKey } from '#src/types'
import descriptor from './descriptor'

/**
* Returns a boolean indicating if `key` is an enumerable own property of a
* given value.
*
* @todo examples
*
* @template T - Target value
* @template K - Property to check
*
* @param {T} target - Target value
* @param {K} key - Property to check
* @return {boolean} `true` if `key` is enumerable own property of `target`
*/
const enumerable = <T, K extends PropertyKey>(target: T, key: K): boolean => {
return !!descriptor(target, key).enumerable
}

export default enumerable
Loading

0 comments on commit b22f86f

Please sign in to comment.