diff --git a/packages/commons/package.json b/packages/commons/package.json index 430e78c51b..191397ce26 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -40,6 +40,10 @@ "default": "./lib/esm/index.js" } }, + "./typeutils": { + "import": "./lib/esm/typeUtils.js", + "require": "./lib/cjs/typeUtils.js" + }, "./utils/base64": { "import": "./lib/esm/fromBase64.js", "require": "./lib/cjs/fromBase64.js" @@ -51,6 +55,10 @@ }, "typesVersions": { "*": { + "typeutils": [ + "lib/cjs/typeUtils.d.ts", + "lib/esm/typeUtils.d.ts" + ], "utils/base64": [ "lib/cjs/fromBase64.d.ts", "lib/esm/fromBase64.d.ts" diff --git a/packages/commons/src/guards.ts b/packages/commons/src/guards.ts deleted file mode 100644 index 7776280e8b..0000000000 --- a/packages/commons/src/guards.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Returns true if the passed value is a record (object). - * - * @param value - */ -const isRecord = (value: unknown): value is Record => { - return ( - Object.prototype.toString.call(value) === '[object Object]' && - !Object.is(value, null) - ); -}; - -/** - * Returns true if the passed value is truthy. - * - * @param value - */ -const isTruthy = (value: unknown): boolean => { - if (typeof value === 'string') { - return value !== ''; - } else if (typeof value === 'number') { - return value !== 0; - } else if (typeof value === 'boolean') { - return value; - } else if (Array.isArray(value)) { - return value.length > 0; - } else if (isRecord(value)) { - return Object.keys(value).length > 0; - } else { - return false; - } -}; - -/** - * Returns true if the passed value is null or undefined. - * - * @param value - */ -const isNullOrUndefined = (value: unknown): value is null | undefined => { - return Object.is(value, null) || Object.is(value, undefined); -}; - -/** - * Returns true if the passed value is a string. - * @param value - * @returns - */ -const isString = (value: unknown): value is string => { - return typeof value === 'string'; -}; - -export { isRecord, isString, isTruthy, isNullOrUndefined }; diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index cdf0815bad..591ad8ccff 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -1,4 +1,14 @@ -export { isRecord, isString, isTruthy, isNullOrUndefined } from './guards.js'; +export { + isRecord, + isString, + isNumber, + isIntegerNumber, + isTruthy, + isNull, + isNullOrUndefined, + getType, + isStrictEqual, +} from './typeUtils.js'; export { Utility } from './Utility.js'; export { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js'; export { addUserAgentMiddleware, isSdkClient } from './awsSdkUtils.js'; diff --git a/packages/commons/src/typeUtils.ts b/packages/commons/src/typeUtils.ts new file mode 100644 index 0000000000..6611aea46c --- /dev/null +++ b/packages/commons/src/typeUtils.ts @@ -0,0 +1,179 @@ +/** + * Returns true if the passed value is a record (object). + * + * @param value The value to check + */ +const isRecord = ( + value: unknown +): value is Record => { + return ( + Object.prototype.toString.call(value) === '[object Object]' && + !Object.is(value, null) + ); +}; + +/** + * Check if a value is a string. + * + * @param value The value to check + */ +const isString = (value: unknown): value is string => { + return typeof value === 'string'; +}; + +/** + * Check if a value is a number. + * + * @param value The value to check + */ +const isNumber = (value: unknown): value is number => { + return typeof value === 'number'; +}; + +/** + * Check if a value is an integer number. + * + * @param value The value to check + */ +const isIntegerNumber = (value: unknown): value is number => { + return isNumber(value) && Number.isInteger(value); +}; + +/** + * Check if a value is truthy. + * + * @param value The value to check + */ +const isTruthy = (value: unknown): boolean => { + if (isString(value)) { + return value !== ''; + } else if (isNumber(value)) { + return value !== 0; + } else if (typeof value === 'boolean') { + return value; + } else if (Array.isArray(value)) { + return value.length > 0; + } else if (isRecord(value)) { + return Object.keys(value).length > 0; + } else { + return false; + } +}; + +/** + * Check if a value is null. + * + * @param value The value to check + */ +const isNull = (value: unknown): value is null => { + return Object.is(value, null); +}; + +/** + * Check if a value is null or undefined. + * + * @param value The value to check + */ +const isNullOrUndefined = (value: unknown): value is null | undefined => { + return isNull(value) || Object.is(value, undefined); +}; + +/** + * Get the type of a value as a string. + * + * @param value The value to check + */ +const getType = (value: unknown): string => { + if (Array.isArray(value)) { + return 'array'; + } else if (isRecord(value)) { + return 'object'; + } else if (isString(value)) { + return 'string'; + } else if (isNumber(value)) { + return 'number'; + } else if (typeof value === 'boolean') { + return 'boolean'; + } else if (isNull(value)) { + return 'null'; + } else { + return 'unknown'; + } +}; + +/** + * Compare two arrays for strict equality. + * + * @param left The left array to compare + * @param right The right array to compare + */ +const areArraysEqual = (left: unknown[], right: unknown[]): boolean => { + if (left.length !== right.length) { + return false; + } + + return left.every((value, i) => isStrictEqual(value, right[i])); +}; + +/** + * Compare two records for strict equality. + * + * @param left The left record to compare + * @param right The right record to compare + */ +const areRecordsEqual = ( + left: Record, + right: Record +): boolean => { + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); + + if (leftKeys.length !== rightKeys.length) { + return false; + } + + return leftKeys.every((key) => isStrictEqual(left[key], right[key])); +}; + +/** + * Check if two unknown values are strictly equal. + * + * If the values are arrays, then each element is compared, regardless of + * order. If the values are objects, then each key and value from left + * is compared to the corresponding key and value from right. If the + * values are primitives, then they are compared using strict equality. + * + * @param left Left side of strict equality comparison + * @param right Right side of strict equality comparison + */ +const isStrictEqual = (left: unknown, right: unknown): boolean => { + if (left === right) { + return true; + } + + if (typeof left !== typeof right) { + return false; + } + + if (Array.isArray(left) && Array.isArray(right)) { + return areArraysEqual(left, right); + } + + if (isRecord(left) && isRecord(right)) { + return areRecordsEqual(left, right); + } + + return false; +}; + +export { + isRecord, + isString, + isNumber, + isIntegerNumber, + isTruthy, + isNull, + isNullOrUndefined, + getType, + isStrictEqual, +}; diff --git a/packages/commons/tests/unit/guards.test.ts b/packages/commons/tests/unit/guards.test.ts deleted file mode 100644 index 53e248e643..0000000000 --- a/packages/commons/tests/unit/guards.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Test guards functions - * - * @group unit/commons/guards - */ -import { - isRecord, - isTruthy, - isNullOrUndefined, - isString, -} from '../../src/index.js'; - -describe('Functions: guards', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.resetModules(); - }); - - describe('Function: isRecord', () => { - it('returns true when the passed object is a Record', () => { - // Prepare - const obj = { a: 1, b: 2, c: 3 }; - - // Act - const result = isRecord(obj); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed object is not a Record', () => { - // Prepare - const obj = [1, 2, 3]; - - // Act - const result = isRecord(obj); - - // Assert - expect(result).toBe(false); - }); - }); - - describe('Function: isTruthy', () => { - it.each(['hello', 1, true, [1], { foo: 1 }])( - 'returns true when the passed value is truthy', - (testValue) => { - // Prepare - const value = testValue; - - // Act - const result = isTruthy(value); - - // Assert - expect(result).toBe(true); - } - ); - - it.each(['', 0, false, [], {}, Symbol])( - 'returns true when the passed value is falsy', - (testValue) => { - // Prepare - const value = testValue; - - // Act - const result = isTruthy(value); - - // Assert - expect(result).toBe(false); - } - ); - }); - - describe('Function: isNullOrUndefined', () => { - it('returns true when the passed value is null or undefined', () => { - // Prepare - const value = undefined; - - // Act - const result = isNullOrUndefined(value); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed value is not null or undefined', () => { - // Prepare - const value = 'hello'; - - // Act - const result = isNullOrUndefined(value); - - // Assert - expect(result).toBe(false); - }); - }); - - describe('Function: isString', () => { - it('returns true when the passed value is a string', () => { - // Prepare - const value = 'hello'; - - // Act - const result = isString(value); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed value is not a string', () => { - // Prepare - const value = 123; - - // Act - const result = isString(value); - - // Assert - expect(result).toBe(false); - }); - }); -}); diff --git a/packages/commons/tests/unit/typeUtils.test.ts b/packages/commons/tests/unit/typeUtils.test.ts new file mode 100644 index 0000000000..7bb940f343 --- /dev/null +++ b/packages/commons/tests/unit/typeUtils.test.ts @@ -0,0 +1,329 @@ +/** + * Test type utils functions + * + * @group unit/commons/typeUtils + */ +import { + isRecord, + isTruthy, + isNullOrUndefined, + isString, + isNumber, + isIntegerNumber, + isNull, + getType, + isStrictEqual, +} from '../../src/index.js'; + +describe('Functions: typeUtils', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe('Function: isRecord', () => { + it('returns true when the passed object is a Record', () => { + // Prepare + const obj = { a: 1, b: 2, c: 3 }; + + // Act + const result = isRecord(obj); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed object is not a Record', () => { + // Prepare + const obj = [1, 2, 3]; + + // Act + const result = isRecord(obj); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isTruthy', () => { + it.each(['hello', 1, true, [1], { foo: 1 }])( + 'returns true when the passed value is truthy', + (testValue) => { + // Prepare + const value = testValue; + + // Act + const result = isTruthy(value); + + // Assess + expect(result).toBe(true); + } + ); + + it.each(['', 0, false, [], {}, Symbol])( + 'returns false when the passed value is falsy', + (testValue) => { + // Prepare + const value = testValue; + + // Act + const result = isTruthy(value); + + // Assess + expect(result).toBe(false); + } + ); + }); + + describe('Function: isNullOrUndefined', () => { + it('returns true when the passed value is null or undefined', () => { + // Prepare + const value = undefined; + + // Act + const result = isNullOrUndefined(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not null or undefined', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNullOrUndefined(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isString', () => { + it('returns true when the passed value is a string', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isString(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not a string', () => { + // Prepare + const value = 123; + + // Act + const result = isString(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isNumber', () => { + it('returns true when the passed value is a number', () => { + // Prepare + const value = 123; + + // Act + const result = isNumber(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not a number', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNumber(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isIntegerNumber', () => { + it('returns true when the passed value is an integer number', () => { + // Prepare + const value = 123; + + // Act + const result = isIntegerNumber(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not an integer number', () => { + // Prepare + const value = 123.45; + + // Act + const result = isIntegerNumber(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isNull', () => { + it('returns true when the passed value is null', () => { + // Prepare + const value = null; + + // Act + const result = isNull(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not null', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNull(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: getType', () => { + it.each([ + { + value: [], + expected: 'array', + }, + { + value: {}, + expected: 'object', + }, + { + value: 'hello', + expected: 'string', + }, + { + value: 123, + expected: 'number', + }, + { + value: true, + expected: 'boolean', + }, + { + value: null, + expected: 'null', + }, + { + value: undefined, + expected: 'unknown', + }, + ])( + 'returns the correct type when passed type $expected', + ({ value, expected }) => { + // Act + const result = getType(value); + + // Assess + expect(result).toBe(expected); + } + ); + }); + + describe('Function: isStrictEqual', () => { + it('returns true when the passed values are strictly equal', () => { + // Prepare + const value1 = 123; + const value2 = 123; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns true when the passed arrays are strictly equal', () => { + // Prepare + const value1 = [1, 2, 3]; + const value2 = [1, 2, 3]; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns true when the passed objects are strictly equal', () => { + // Prepare + const value1 = { a: 1, b: 2, c: 3 }; + const value2 = { a: 1, b: 2, c: 3 }; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed values are not strictly equal', () => { + // Prepare + const value1 = 123; + const value2 = '123'; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + }); + + it.each([ + { + value1: [1, 2, 3], + value2: [1, 3, 2], + }, + { + value1: [1, 2, 3], + value2: [1, 2], + }, + ])( + 'returns false when the passed arrays are not strictly equal', + ({ value1, value2 }) => { + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + } + ); + + it.each([ + { + value1: { a: 1, b: 2, c: 3 }, + value2: { a: 1, b: 3, c: 2 }, + }, + { + value1: { a: 1, b: 2, c: 3 }, + value2: { a: 1, b: 2 }, + }, + ])( + 'returns false when the passed objects are not strictly equal', + ({ value1, value2 }) => { + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + } + ); + }); +});