Skip to content

Commit

Permalink
feat(utils): ksort
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Jul 21, 2023
1 parent d2c2018 commit efd4d09
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 13 deletions.
1 change: 1 addition & 0 deletions .dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ iife
infile
jsonifiable
keyid
ksort
larsgw
lcov
lintstagedrc
Expand Down
12 changes: 12 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ const config = {
'@typescript-eslint/ban-types': 0
}
},
{
files: ['src/utils/__tests__/ksort.spec.ts'],
rules: {
'sort-keys': 0
}
},
{
files: ['src/utils/ksort.ts'],
rules: {
'@typescript-eslint/no-use-before-define': 0
}
},
{
files: ['src/utils/noop.ts'],
rules: {
Expand Down
19 changes: 12 additions & 7 deletions src/types/__tests__/object-plain.spec-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ import type TestSubject from '../object-plain'
import type Primitive from '../primitive'

describe('unit-d:types/ObjectPlain', () => {
it('should match [[x: string]: unknown]', () => {
expectTypeOf<TestSubject[string]>().toBeUnknown()
class Person {}

it('should match [[x: string]: Primitive | object]', () => {
expectTypeOf<TestSubject[string]>().toEqualTypeOf<Primitive | object>()
})

it('should match [[x: symbol]: unknown]', () => {
expectTypeOf<TestSubject[symbol]>().toBeUnknown()
it('should match [[x: symbol]: Primitive | object]', () => {
expectTypeOf<TestSubject[symbol]>().toEqualTypeOf<Primitive | object>()
})

it('should match pojos', () => {
expectTypeOf({}).toMatchTypeOf<TestSubject>()
expectTypeOf({ 5: null }).toMatchTypeOf<TestSubject>()
expectTypeOf({ email: faker.internet.email() }).toMatchTypeOf<TestSubject>()
})
Expand All @@ -31,13 +34,15 @@ describe('unit-d:types/ObjectPlain', () => {
})

it('should not match class instance objects', () => {
expectTypeOf(new Date()).not.toMatchTypeOf<TestSubject>()
expectTypeOf(new Map()).not.toMatchTypeOf<TestSubject>()
expectTypeOf(new Set()).not.toMatchTypeOf<TestSubject>()
expectTypeOf<typeof Date>().not.toMatchTypeOf<TestSubject>()
expectTypeOf<typeof Person>().not.toMatchTypeOf<TestSubject>()
expectTypeOf<typeof Map>().not.toMatchTypeOf<TestSubject>()
expectTypeOf<typeof Set>().not.toMatchTypeOf<TestSubject>()
})

it('should not match functions', () => {
expectTypeOf<Fn>().not.toMatchTypeOf<TestSubject>()
expectTypeOf<Readonly<Fn>>().not.toMatchTypeOf<TestSubject>()
})

it('should not match primitives', () => {
Expand Down
9 changes: 7 additions & 2 deletions src/types/object-plain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
* @module tutils/types/ObjectPlain
*/

import type Primitive from './primitive'

/**
* Type representing a plain old JavaScript object (POJO).
* A plain old JavaScript object (POJO).
*
* @see https://masteringjs.io/tutorials/fundamentals/pojo
*/
type ObjectPlain = { [K in string | symbol]?: unknown }
type ObjectPlain = {
[x: string | symbol]: Primitive | object
readonly arguments?: never
}

export type { ObjectPlain as default }
44 changes: 44 additions & 0 deletions src/utils/__snapshots__/ksort.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`unit:utils/ksort > arrays > deep > should return obj with item keys sorted deeply 1`] = `
[
{
"make": "lexus",
"model": "lc",
"vin": "0WBW1G4D29TC62167",
"year": 2023,
},
[Circular],
]
`;

exports[`unit:utils/ksort > plain objects > deep > should return obj with nested and top-level keys sorted 1`] = `
{
"circular": [Circular],
"driver": {
"first_name": "tres",
"last_name": "koelpin",
"nanoid": "mhR2OWSEhQFXkwU1NJY23",
},
"make": "lexus",
"model": "lc",
"riders": [
{
"first_name": "adrian",
"last_name": "rodriguez",
"uuid": "19fd6413-7fc9-4e7f-a5b5-9a3689d25134",
},
],
"vin": "0WBW1G4D29TC62167",
"year": 2023,
}
`;

exports[`unit:utils/ksort > plain objects > should return obj with top-level keys sorted 1`] = `
{
"make": "lexus",
"model": "lc",
"vin": "0WBW1G4D29TC62167",
"year": 2023,
}
`;
20 changes: 20 additions & 0 deletions src/utils/__tests__/ksort.options.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @file Type Tests - KsortOptions
* @module tutils/utils/tests/unit-d/KsortOptions
*/

import type { Nilable } from '#src/types'
import type AlphabetizeOptions from '../alphabetize.options'
import type TestSubject from '../ksort.options'

describe('unit-d:utils/KsortOptions', () => {
it('should extend AlphabetizeOptions', () => {
expectTypeOf<TestSubject>().toMatchTypeOf<AlphabetizeOptions>()
})

it('should match [deep?: Nilable<boolean>]', () => {
expectTypeOf<TestSubject>()
.toHaveProperty('deep')
.toEqualTypeOf<Nilable<boolean>>()
})
})
17 changes: 17 additions & 0 deletions src/utils/__tests__/ksort.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @file Type Tests - ksort
* @module tutils/utils/tests/unit-d/ksort
*/

import type Vehicle from '#fixtures/types/vehicle'
import type testSubject from '../ksort'

describe('unit-d:utils/ksort', () => {
it('should return T', () => {
// Arrange
type T = Vehicle

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

import type Vehicle from '#fixtures/types/vehicle'
import VEHICLE, { VEHICLE_TAG } from '#fixtures/vehicle'
import type { Opaque } from '#src/types'
import cast from '../cast'
import define from '../define'
import testSubject from '../ksort'

describe('unit:utils/ksort', () => {
let vehicle: Opaque<Vehicle, 'vehicle'>

beforeAll(() => {
vehicle = cast({ year: 2023, vin: VEHICLE.vin, model: 'lc', make: 'lexus' })

define(vehicle, VEHICLE_TAG, {
configurable: false,
enumerable: false,
value: 'vehicle',
writable: false
})
})

it('should return obj if obj is not an array or plain object', () => {
// Arrange
const obj: Set<Vehicle> = new Set([vehicle])

// Act + Expect
expect(testSubject(obj)).to.equal(obj)
})

describe('arrays', () => {
it('should return obj if item keys should not be sorted', () => {
// Arrange
const obj: [Vehicle] = [vehicle]

// Act + Expect
expect(testSubject(obj)).to.equal(obj)
})

describe('deep', () => {
let obj: (Vehicle | Vehicle[])[]

beforeAll(() => {
obj = [vehicle]
obj.push(cast(obj))
})

it('should return obj with item keys sorted deeply', () => {
// Act
const result = testSubject(obj, { deep: true })

// Expect
expect(result).to.eql(obj).and.equal(obj)
expect(result).toMatchSnapshot()
})
})
})

describe('plain objects', () => {
it('should return obj with top-level keys sorted', () => {
// Act
const result = testSubject(vehicle)

// Expect
expect(result).to.eql(vehicle).and.equal(vehicle)
expect(result).toMatchSnapshot()
})

describe('deep', () => {
let obj: Vehicle & {
driver: { first_name: string; last_name: string; nanoid: string }
riders: { first_name: string; last_name: string; uuid: string }[]
}

beforeAll(() => {
obj = {
...vehicle,
driver: {
nanoid: 'mhR2OWSEhQFXkwU1NJY23',
last_name: 'koelpin',
first_name: 'tres'
},
riders: [
{
uuid: '19fd6413-7fc9-4e7f-a5b5-9a3689d25134',
last_name: 'rodriguez',
first_name: 'adrian'
}
]
}

define(obj, 'circular', { value: obj })
})

it('should return obj with nested and top-level keys sorted', () => {
// Act
const result = testSubject(obj, { deep: true })

// Expect
expect(result).to.eql(obj).and.equal(obj)
expect(result).toMatchSnapshot()
})
})
})
})
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export { default as isWeakSet } from './is-weak-set'
export { default as join } from './join'
export { default as keys } from './keys'
export type { default as KeysOptions } from './keys.options'
export { default as ksort } from './ksort'
export type { default as KsortOptions } from './ksort.options'
export { default as listify } from './listify'
export { default as lowercase } from './lowercase'
export { default as noop } from './noop'
Expand Down
6 changes: 4 additions & 2 deletions src/utils/is-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import isObject from './is-object'
/**
* Checks if `value` is an array.
*
* @todo examples
*
* @template T - Array item type
*
* @param {unknown} value - Value to check
* @return {value is ReadonlyArray<T> | T[]} `true` if `value` is an array
*/
function isArray<T>(value: unknown): value is T[] | readonly T[] {
return isObject(value) && equal(Reflect.get(value, 'constructor'), Array)
const isArray = <T>(value: unknown): value is T[] | readonly T[] => {
return isObject(value) && equal(value.constructor, Array)
}

export default isArray
6 changes: 4 additions & 2 deletions src/utils/is-object-plain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import equal from './equal'
import isNull from './is-null'

/**
* Checks if `value` is a plain object (i.e. [POJO][1]).
* Checks if `value` is a plain object ([POJO][1]).
*
* A plain object is an object created by the [`Object`][2] constructor or an
* object with a `[[Prototype]]` of `null`.
Expand All @@ -18,12 +18,14 @@ import isNull from './is-null'
*
* @see {@linkcode ObjectPlain}
*
* @todo examples
*
* @param {unknown} value - Value to check
* @return {value is ObjectPlain} `true` if `value` is plain object
*/
const isObjectPlain = (value: unknown): value is ObjectPlain => {
/**
* Plain object check for {@linkcode value}.
* Plain object check.
*
* @var {boolean} plain
*/
Expand Down
25 changes: 25 additions & 0 deletions src/utils/ksort.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @file Utilities - KsortOptions
* @module tutils/utils/ksort/options
*/

import type { Nilable } from '#src/types'
import type AlphabetizeOptions from './alphabetize.options'

/**
* Property key sorting options.
*
* @see {@linkcode AlphabetizeOptions}
*
* @extends {AlphabetizeOptions}
*/
interface KsortOptions extends AlphabetizeOptions {
/**
* Recursively sort keys, including keys of plain objects within arrays.
*
* @default false
*/
deep?: Nilable<boolean>
}

export type { KsortOptions as default }
Loading

0 comments on commit efd4d09

Please sign in to comment.