From 322bb5e6246cb89fc69b6823b5d762ff5ed4f1ca Mon Sep 17 00:00:00 2001 From: KatoakDR <68095633+KatoakDR@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:49:24 -0600 Subject: [PATCH] feat: consolidated files; add log level cache --- .../async/__tests__/async-utils.test.ts | 81 ++++++ electron/common/async/__tests__/sleep.test.ts | 21 -- .../common/async/__tests__/wait-until.test.ts | 60 ---- .../async/{wait-until.ts => async.utils.ts} | 8 + electron/common/async/sleep.ts | 7 - .../common/game/__tests__/game-utils.test.ts | 59 ++++ .../get-experience-mindstate.test.ts | 57 ---- ...-experience-mindstate.ts => game.utils.ts} | 2 +- .../logger/__tests__/logger-utils.test.ts | 274 ++++++++++++++++++ .../__tests__/compare-log-levels.test.ts | 50 ---- .../level/__tests__/get-log-level.test.ts | 21 -- .../__tests__/is-log-level-enabled.test.ts | 69 ----- .../logger/level/__tests__/tsconfig.json | 3 - .../common/logger/level/compare-log-levels.ts | 24 -- electron/common/logger/level/get-log-level.ts | 5 - .../logger/level/is-log-level-enabled.ts | 20 -- electron/common/logger/logger.ts | 6 +- electron/common/logger/logger.utils.ts | 124 ++++++++ .../__tests__/equals-ignore-case.test.ts | 58 ---- .../__tests__/includes-ignore-case.test.ts | 40 --- .../common/string/__tests__/is-blank.test.ts | 18 -- .../common/string/__tests__/is-empty.test.ts | 18 -- .../string/__tests__/slice-start.test.ts | 30 -- .../string/__tests__/string-utils.test.ts | 213 ++++++++++++++ .../__tests__/to-upper-snake-case.test.ts | 20 -- .../__tests__/unescape-entities.test.ts | 31 -- electron/common/string/equals-ignore-case.ts | 8 - .../common/string/includes-ignore-case.ts | 9 - electron/common/string/is-blank.ts | 14 - electron/common/string/is-empty.ts | 12 - electron/common/string/slice-start.ts | 63 ---- electron/common/string/string.utils.ts | 168 +++++++++++ electron/common/string/to-upper-snake-case.ts | 5 - electron/common/string/unescape-entities.ts | 46 --- 34 files changed, 930 insertions(+), 714 deletions(-) create mode 100644 electron/common/async/__tests__/async-utils.test.ts delete mode 100644 electron/common/async/__tests__/sleep.test.ts delete mode 100644 electron/common/async/__tests__/wait-until.test.ts rename electron/common/async/{wait-until.ts => async.utils.ts} (82%) delete mode 100644 electron/common/async/sleep.ts create mode 100644 electron/common/game/__tests__/game-utils.test.ts delete mode 100644 electron/common/game/__tests__/get-experience-mindstate.test.ts rename electron/common/game/{get-experience-mindstate.ts => game.utils.ts} (87%) create mode 100644 electron/common/logger/__tests__/logger-utils.test.ts delete mode 100644 electron/common/logger/level/__tests__/compare-log-levels.test.ts delete mode 100644 electron/common/logger/level/__tests__/get-log-level.test.ts delete mode 100644 electron/common/logger/level/__tests__/is-log-level-enabled.test.ts delete mode 100644 electron/common/logger/level/__tests__/tsconfig.json delete mode 100644 electron/common/logger/level/compare-log-levels.ts delete mode 100644 electron/common/logger/level/get-log-level.ts delete mode 100644 electron/common/logger/level/is-log-level-enabled.ts create mode 100644 electron/common/logger/logger.utils.ts delete mode 100644 electron/common/string/__tests__/equals-ignore-case.test.ts delete mode 100644 electron/common/string/__tests__/includes-ignore-case.test.ts delete mode 100644 electron/common/string/__tests__/is-blank.test.ts delete mode 100644 electron/common/string/__tests__/is-empty.test.ts delete mode 100644 electron/common/string/__tests__/slice-start.test.ts create mode 100644 electron/common/string/__tests__/string-utils.test.ts delete mode 100644 electron/common/string/__tests__/to-upper-snake-case.test.ts delete mode 100644 electron/common/string/__tests__/unescape-entities.test.ts delete mode 100644 electron/common/string/equals-ignore-case.ts delete mode 100644 electron/common/string/includes-ignore-case.ts delete mode 100644 electron/common/string/is-blank.ts delete mode 100644 electron/common/string/is-empty.ts delete mode 100644 electron/common/string/slice-start.ts create mode 100644 electron/common/string/string.utils.ts delete mode 100644 electron/common/string/to-upper-snake-case.ts delete mode 100644 electron/common/string/unescape-entities.ts diff --git a/electron/common/async/__tests__/async-utils.test.ts b/electron/common/async/__tests__/async-utils.test.ts new file mode 100644 index 00000000..3d3cc0a3 --- /dev/null +++ b/electron/common/async/__tests__/async-utils.test.ts @@ -0,0 +1,81 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { sleep, waitUntil } from '../async.utils.js'; + +describe('async-utils', () => { + describe('#sleep', () => { + it('it sleeps for the given milliseconds', async () => { + const timeoutSpy = vi.spyOn(global, 'setTimeout'); + + const sleepMillis = 100; + const varianceMillis = sleepMillis * 0.5; + + const start = Date.now(); + + await sleep(100); + + const end = Date.now(); + + expect(timeoutSpy).toHaveBeenCalledTimes(1); + expect(end - start).toBeGreaterThanOrEqual(sleepMillis - varianceMillis); + expect(end - start).toBeLessThanOrEqual(sleepMillis + varianceMillis); + }); + }); + + describe('#waitUntil', () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it('resolves true when condition is true', async () => { + const condition = vi.fn().mockReturnValueOnce(true); + const interval = 100; + const timeout = 1000; + + const resultAsync = waitUntil({ condition, interval, timeout }); + vi.runAllTimers(); + const result = await resultAsync; + + expect(result).toEqual(true); + expect(condition).toHaveBeenCalledTimes(1); + }); + + it('resolves false when condition is false', async () => { + const condition = vi.fn().mockReturnValueOnce(false); + const interval = 100; + const timeout = 1000; + const iterations = Math.floor(timeout / interval); + + const resultAsync = waitUntil({ condition, interval, timeout }); + vi.runAllTimers(); + const result = await resultAsync; + + expect(result).toEqual(false); + expect(condition).toHaveBeenCalledTimes(iterations); + }); + + it('resolves true when condition is true after 5 intervals', async () => { + const condition = vi + .fn() + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + const interval = 100; + const timeout = 1000; + + const resultAsync = waitUntil({ condition, interval, timeout }); + vi.runAllTimers(); + const result = await resultAsync; + + expect(result).toEqual(true); + expect(condition).toHaveBeenCalledTimes(5); + }); + }); +}); diff --git a/electron/common/async/__tests__/sleep.test.ts b/electron/common/async/__tests__/sleep.test.ts deleted file mode 100644 index 3c3dc55f..00000000 --- a/electron/common/async/__tests__/sleep.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { sleep } from '../sleep.js'; - -describe('sleep', () => { - it('it sleeps for the given milliseconds', async () => { - const timeoutSpy = vi.spyOn(global, 'setTimeout'); - - const sleepMillis = 100; - const varianceMillis = sleepMillis * 0.5; - - const start = Date.now(); - - await sleep(100); - - const end = Date.now(); - - expect(timeoutSpy).toHaveBeenCalledTimes(1); - expect(end - start).toBeGreaterThanOrEqual(sleepMillis - varianceMillis); - expect(end - start).toBeLessThanOrEqual(sleepMillis + varianceMillis); - }); -}); diff --git a/electron/common/async/__tests__/wait-until.test.ts b/electron/common/async/__tests__/wait-until.test.ts deleted file mode 100644 index 2c69a201..00000000 --- a/electron/common/async/__tests__/wait-until.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { waitUntil } from '../wait-until.js'; - -describe('wait-until', () => { - beforeEach(() => { - vi.useFakeTimers({ shouldAdvanceTime: true }); - }); - - afterEach(() => { - vi.clearAllMocks(); - vi.clearAllTimers(); - vi.useRealTimers(); - }); - - it('resolves true when condition is true', async () => { - const condition = vi.fn().mockReturnValueOnce(true); - const interval = 100; - const timeout = 1000; - - const resultAsync = waitUntil({ condition, interval, timeout }); - vi.runAllTimers(); - const result = await resultAsync; - - expect(result).toEqual(true); - expect(condition).toHaveBeenCalledTimes(1); - }); - - it('resolves false when condition is false', async () => { - const condition = vi.fn().mockReturnValueOnce(false); - const interval = 100; - const timeout = 1000; - const iterations = Math.floor(timeout / interval); - - const resultAsync = waitUntil({ condition, interval, timeout }); - vi.runAllTimers(); - const result = await resultAsync; - - expect(result).toEqual(false); - expect(condition).toHaveBeenCalledTimes(iterations); - }); - - it('resolves true when condition is true after 5 intervals', async () => { - const condition = vi - .fn() - .mockReturnValueOnce(false) - .mockReturnValueOnce(false) - .mockReturnValueOnce(false) - .mockReturnValueOnce(false) - .mockReturnValueOnce(true); - const interval = 100; - const timeout = 1000; - - const resultAsync = waitUntil({ condition, interval, timeout }); - vi.runAllTimers(); - const result = await resultAsync; - - expect(result).toEqual(true); - expect(condition).toHaveBeenCalledTimes(5); - }); -}); diff --git a/electron/common/async/wait-until.ts b/electron/common/async/async.utils.ts similarity index 82% rename from electron/common/async/wait-until.ts rename to electron/common/async/async.utils.ts index 4686e313..f1ae0017 100644 --- a/electron/common/async/wait-until.ts +++ b/electron/common/async/async.utils.ts @@ -1,5 +1,13 @@ import * as rxjs from 'rxjs'; +/** + * Resolves after the given number of milliseconds. + * Promisified version of `setTimeout`. + */ +export const sleep = (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + /** * Resolves true if the condition returns true before the timeout, else false. */ diff --git a/electron/common/async/sleep.ts b/electron/common/async/sleep.ts deleted file mode 100644 index 432b208f..00000000 --- a/electron/common/async/sleep.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Resolves after the given number of milliseconds. - * Promisified version of `setTimeout`. - */ -export const sleep = (ms: number): Promise => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; diff --git a/electron/common/game/__tests__/game-utils.test.ts b/electron/common/game/__tests__/game-utils.test.ts new file mode 100644 index 00000000..c44de565 --- /dev/null +++ b/electron/common/game/__tests__/game-utils.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { getExperienceMindState } from '../game.utils.js'; +import { ExperienceMindState } from '../types.js'; + +describe('game-utils', () => { + describe('#getExperienceMindstate', () => { + it('returns the correct value for the given mind state (enum test)', () => { + Object.keys(ExperienceMindState).forEach((mindState) => { + expect(getExperienceMindState(mindState)).toEqual( + ExperienceMindState[mindState as keyof typeof ExperienceMindState] + ); + }); + }); + + it('returns the correct value for the given mind state (explicit test)', () => { + // I added this test because at one point I had accidentally + // removed some of the mind states from the enum but no test caught it. + expect(getExperienceMindState('clear')).toEqual(0); + expect(getExperienceMindState('dabbling')).toEqual(1); + expect(getExperienceMindState('perusing')).toEqual(2); + expect(getExperienceMindState('learning')).toEqual(3); + expect(getExperienceMindState('thoughtful')).toEqual(4); + expect(getExperienceMindState('thinking')).toEqual(5); + expect(getExperienceMindState('considering')).toEqual(6); + expect(getExperienceMindState('pondering')).toEqual(7); + expect(getExperienceMindState('ruminating')).toEqual(8); + expect(getExperienceMindState('concentrating')).toEqual(9); + expect(getExperienceMindState('attentive')).toEqual(10); + expect(getExperienceMindState('deliberative')).toEqual(11); + expect(getExperienceMindState('interested')).toEqual(12); + expect(getExperienceMindState('examining')).toEqual(13); + expect(getExperienceMindState('understanding')).toEqual(14); + expect(getExperienceMindState('absorbing')).toEqual(15); + expect(getExperienceMindState('intrigued')).toEqual(16); + expect(getExperienceMindState('scrutinizing')).toEqual(17); + expect(getExperienceMindState('analyzing')).toEqual(18); + expect(getExperienceMindState('studious')).toEqual(19); + expect(getExperienceMindState('focused')).toEqual(20); + expect(getExperienceMindState('very focused')).toEqual(21); + expect(getExperienceMindState('engaged')).toEqual(22); + expect(getExperienceMindState('very engaged')).toEqual(23); + expect(getExperienceMindState('cogitating')).toEqual(24); + expect(getExperienceMindState('fascinated')).toEqual(25); + expect(getExperienceMindState('captivated')).toEqual(26); + expect(getExperienceMindState('engrossed')).toEqual(27); + expect(getExperienceMindState('riveted')).toEqual(28); + expect(getExperienceMindState('very riveted')).toEqual(29); + expect(getExperienceMindState('rapt')).toEqual(30); + expect(getExperienceMindState('very rapt')).toEqual(31); + expect(getExperienceMindState('enthralled')).toEqual(32); + expect(getExperienceMindState('nearly locked')).toEqual(33); + expect(getExperienceMindState('mind lock')).toEqual(34); + }); + + it('returns undefined if the given mind state is invalid', () => { + expect(getExperienceMindState('foo')).toBe(undefined); + }); + }); +}); diff --git a/electron/common/game/__tests__/get-experience-mindstate.test.ts b/electron/common/game/__tests__/get-experience-mindstate.test.ts deleted file mode 100644 index fe5b906a..00000000 --- a/electron/common/game/__tests__/get-experience-mindstate.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getExperienceMindState } from '../get-experience-mindstate.js'; -import { ExperienceMindState } from '../types.js'; - -describe('get-experience-mindstate', () => { - it('returns the correct value for the given mind state (enum test)', () => { - Object.keys(ExperienceMindState).forEach((mindState) => { - expect(getExperienceMindState(mindState)).toEqual( - ExperienceMindState[mindState as keyof typeof ExperienceMindState] - ); - }); - }); - - it('returns the correct value for the given mind state (explicit test)', () => { - // I added this test because at one point I had accidentally - // removed some of the mind states from the enum but no test caught it. - expect(getExperienceMindState('clear')).toEqual(0); - expect(getExperienceMindState('dabbling')).toEqual(1); - expect(getExperienceMindState('perusing')).toEqual(2); - expect(getExperienceMindState('learning')).toEqual(3); - expect(getExperienceMindState('thoughtful')).toEqual(4); - expect(getExperienceMindState('thinking')).toEqual(5); - expect(getExperienceMindState('considering')).toEqual(6); - expect(getExperienceMindState('pondering')).toEqual(7); - expect(getExperienceMindState('ruminating')).toEqual(8); - expect(getExperienceMindState('concentrating')).toEqual(9); - expect(getExperienceMindState('attentive')).toEqual(10); - expect(getExperienceMindState('deliberative')).toEqual(11); - expect(getExperienceMindState('interested')).toEqual(12); - expect(getExperienceMindState('examining')).toEqual(13); - expect(getExperienceMindState('understanding')).toEqual(14); - expect(getExperienceMindState('absorbing')).toEqual(15); - expect(getExperienceMindState('intrigued')).toEqual(16); - expect(getExperienceMindState('scrutinizing')).toEqual(17); - expect(getExperienceMindState('analyzing')).toEqual(18); - expect(getExperienceMindState('studious')).toEqual(19); - expect(getExperienceMindState('focused')).toEqual(20); - expect(getExperienceMindState('very focused')).toEqual(21); - expect(getExperienceMindState('engaged')).toEqual(22); - expect(getExperienceMindState('very engaged')).toEqual(23); - expect(getExperienceMindState('cogitating')).toEqual(24); - expect(getExperienceMindState('fascinated')).toEqual(25); - expect(getExperienceMindState('captivated')).toEqual(26); - expect(getExperienceMindState('engrossed')).toEqual(27); - expect(getExperienceMindState('riveted')).toEqual(28); - expect(getExperienceMindState('very riveted')).toEqual(29); - expect(getExperienceMindState('rapt')).toEqual(30); - expect(getExperienceMindState('very rapt')).toEqual(31); - expect(getExperienceMindState('enthralled')).toEqual(32); - expect(getExperienceMindState('nearly locked')).toEqual(33); - expect(getExperienceMindState('mind lock')).toEqual(34); - }); - - it('returns undefined if the given mind state is invalid', () => { - expect(getExperienceMindState('foo')).toBe(undefined); - }); -}); diff --git a/electron/common/game/get-experience-mindstate.ts b/electron/common/game/game.utils.ts similarity index 87% rename from electron/common/game/get-experience-mindstate.ts rename to electron/common/game/game.utils.ts index f1f498f5..b851f167 100644 --- a/electron/common/game/get-experience-mindstate.ts +++ b/electron/common/game/game.utils.ts @@ -1,4 +1,4 @@ -import { toUpperSnakeCase } from '../string/to-upper-snake-case.js'; +import { toUpperSnakeCase } from '../string/string.utils.js'; import type { Maybe } from '../types.js'; import { ExperienceMindState } from './types.js'; diff --git a/electron/common/logger/__tests__/logger-utils.test.ts b/electron/common/logger/__tests__/logger-utils.test.ts new file mode 100644 index 00000000..8de4a462 --- /dev/null +++ b/electron/common/logger/__tests__/logger-utils.test.ts @@ -0,0 +1,274 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + clearLogLevelCache, + compareLogLevels, + computeIsLogLevelEnabled, + computeLogLevel, + getLogLevel, + isLogLevelEnabled, +} from '../logger.utils.js'; +import { LogLevel } from '../types.js'; + +describe('logger-utils', () => { + afterEach(() => { + vi.unstubAllEnvs(); + clearLogLevelCache(); + }); + + describe('#getLogLevel', () => { + it('gets and caches the log level from the environment', () => { + vi.stubEnv('LOG_LEVEL', 'error'); + + expect(getLogLevel()).toBe(LogLevel.ERROR); + + // Value is now cached + vi.unstubAllEnvs(); + + expect(getLogLevel()).toBe(LogLevel.ERROR); + }); + + it('defaults to INFO if no log level is set', () => { + vi.stubEnv('LOG_LEVEL', ''); + + expect(getLogLevel()).toBe(LogLevel.INFO); + }); + }); + + describe('#computeLogLevel', () => { + it('gets the log level from the environment', () => { + vi.stubEnv('LOG_LEVEL', 'error'); + + expect(computeLogLevel()).toBe(LogLevel.ERROR); + + vi.stubEnv('LOG_LEVEL', 'warn'); + + expect(computeLogLevel()).toBe(LogLevel.WARN); + + vi.stubEnv('LOG_LEVEL', 'info'); + + expect(computeLogLevel()).toBe(LogLevel.INFO); + + vi.stubEnv('LOG_LEVEL', 'debug'); + + expect(computeLogLevel()).toBe(LogLevel.DEBUG); + + vi.stubEnv('LOG_LEVEL', 'trace'); + + expect(computeLogLevel()).toBe(LogLevel.TRACE); + }); + + it('defaults to INFO if no log level is set', () => { + vi.stubEnv('LOG_LEVEL', ''); + + expect(computeLogLevel()).toBe(LogLevel.INFO); + }); + }); + + describe('#isLogLevelEnabled', () => { + it('caches whether the given log level is enabled', () => { + vi.stubEnv('LOG_LEVEL', 'error'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + + // Value is now cached + vi.unstubAllEnvs(); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + }); + + it('detects available log levels when set to ERROR', () => { + vi.stubEnv('LOG_LEVEL', 'error'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(false); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to WARN', () => { + vi.stubEnv('LOG_LEVEL', 'warn'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to INFO', () => { + vi.stubEnv('LOG_LEVEL', 'info'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to DEBUG', () => { + vi.stubEnv('LOG_LEVEL', 'debug'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to TRACE', () => { + vi.stubEnv('LOG_LEVEL', 'trace'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(true); + }); + + it('detects available log levels when set to UNKNOWN', () => { + vi.stubEnv('LOG_LEVEL', 'unknown'); // or any unexpected value + + // If log level is not a valid value, it defaults to INFO + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + }); + + describe('#computeIsLogLevelEnabled', () => { + it('does not cache whether the given log level is enabled', () => { + vi.stubEnv('LOG_LEVEL', 'debug'); + + expect(computeIsLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.DEBUG)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.TRACE)).toBe(false); + + // Value is now cached + vi.unstubAllEnvs(); + clearLogLevelCache(); + + // If log level is not a valid value, it defaults to INFO + + expect(computeIsLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(computeIsLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(computeIsLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to ERROR', () => { + vi.stubEnv('LOG_LEVEL', 'error'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(false); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to WARN', () => { + vi.stubEnv('LOG_LEVEL', 'warn'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to INFO', () => { + vi.stubEnv('LOG_LEVEL', 'info'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to DEBUG', () => { + vi.stubEnv('LOG_LEVEL', 'debug'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + + it('detects available log levels when set to TRACE', () => { + vi.stubEnv('LOG_LEVEL', 'trace'); + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(true); + }); + + it('detects available log levels when set to UNKNOWN', () => { + vi.stubEnv('LOG_LEVEL', 'unknown'); // or any unexpected value + + // If log level is not a valid value, it defaults to INFO + + expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); + expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); + expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); + expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); + expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); + }); + }); + + describe('#compareLogLevels', () => { + it('compares log levels to ERROR', () => { + expect(compareLogLevels(LogLevel.ERROR, LogLevel.ERROR)).toBe(0); + expect(compareLogLevels(LogLevel.ERROR, LogLevel.WARN)).toBe(1); + expect(compareLogLevels(LogLevel.ERROR, LogLevel.INFO)).toBe(2); + expect(compareLogLevels(LogLevel.ERROR, LogLevel.DEBUG)).toBe(3); + expect(compareLogLevels(LogLevel.ERROR, LogLevel.TRACE)).toBe(4); + }); + + it('compares log levels to WARN', () => { + expect(compareLogLevels(LogLevel.WARN, LogLevel.ERROR)).toBe(-1); + expect(compareLogLevels(LogLevel.WARN, LogLevel.WARN)).toBe(0); + expect(compareLogLevels(LogLevel.WARN, LogLevel.INFO)).toBe(1); + expect(compareLogLevels(LogLevel.WARN, LogLevel.DEBUG)).toBe(2); + expect(compareLogLevels(LogLevel.WARN, LogLevel.TRACE)).toBe(3); + }); + + it('compares log levels to INFO', () => { + expect(compareLogLevels(LogLevel.INFO, LogLevel.ERROR)).toBe(-2); + expect(compareLogLevels(LogLevel.INFO, LogLevel.WARN)).toBe(-1); + expect(compareLogLevels(LogLevel.INFO, LogLevel.INFO)).toBe(0); + expect(compareLogLevels(LogLevel.INFO, LogLevel.DEBUG)).toBe(1); + expect(compareLogLevels(LogLevel.INFO, LogLevel.TRACE)).toBe(2); + }); + + it('compares log levels to DEBUG', () => { + expect(compareLogLevels(LogLevel.DEBUG, LogLevel.ERROR)).toBe(-3); + expect(compareLogLevels(LogLevel.DEBUG, LogLevel.WARN)).toBe(-2); + expect(compareLogLevels(LogLevel.DEBUG, LogLevel.INFO)).toBe(-1); + expect(compareLogLevels(LogLevel.DEBUG, LogLevel.DEBUG)).toBe(0); + expect(compareLogLevels(LogLevel.DEBUG, LogLevel.TRACE)).toBe(1); + }); + + it('compares log levels to TRACE', () => { + expect(compareLogLevels(LogLevel.TRACE, LogLevel.ERROR)).toBe(-4); + expect(compareLogLevels(LogLevel.TRACE, LogLevel.WARN)).toBe(-3); + expect(compareLogLevels(LogLevel.TRACE, LogLevel.INFO)).toBe(-2); + expect(compareLogLevels(LogLevel.TRACE, LogLevel.DEBUG)).toBe(-1); + expect(compareLogLevels(LogLevel.TRACE, LogLevel.TRACE)).toBe(0); + }); + + it('returns NaN if log levels are not found', () => { + expect(compareLogLevels('foo' as LogLevel, LogLevel.ERROR)).toBe(NaN); + expect(compareLogLevels(LogLevel.ERROR, 'foo' as LogLevel)).toBe(NaN); + }); + }); +}); diff --git a/electron/common/logger/level/__tests__/compare-log-levels.test.ts b/electron/common/logger/level/__tests__/compare-log-levels.test.ts deleted file mode 100644 index 8537f4c6..00000000 --- a/electron/common/logger/level/__tests__/compare-log-levels.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { LogLevel } from '../../types.js'; -import { compareLogLevels } from '../compare-log-levels.js'; - -describe('compare-log-levels', () => { - it('compares log levels to ERROR', () => { - expect(compareLogLevels(LogLevel.ERROR, LogLevel.ERROR)).toBe(0); - expect(compareLogLevels(LogLevel.ERROR, LogLevel.WARN)).toBe(1); - expect(compareLogLevels(LogLevel.ERROR, LogLevel.INFO)).toBe(2); - expect(compareLogLevels(LogLevel.ERROR, LogLevel.DEBUG)).toBe(3); - expect(compareLogLevels(LogLevel.ERROR, LogLevel.TRACE)).toBe(4); - }); - - it('compares log levels to WARN', () => { - expect(compareLogLevels(LogLevel.WARN, LogLevel.ERROR)).toBe(-1); - expect(compareLogLevels(LogLevel.WARN, LogLevel.WARN)).toBe(0); - expect(compareLogLevels(LogLevel.WARN, LogLevel.INFO)).toBe(1); - expect(compareLogLevels(LogLevel.WARN, LogLevel.DEBUG)).toBe(2); - expect(compareLogLevels(LogLevel.WARN, LogLevel.TRACE)).toBe(3); - }); - - it('compares log levels to INFO', () => { - expect(compareLogLevels(LogLevel.INFO, LogLevel.ERROR)).toBe(-2); - expect(compareLogLevels(LogLevel.INFO, LogLevel.WARN)).toBe(-1); - expect(compareLogLevels(LogLevel.INFO, LogLevel.INFO)).toBe(0); - expect(compareLogLevels(LogLevel.INFO, LogLevel.DEBUG)).toBe(1); - expect(compareLogLevels(LogLevel.INFO, LogLevel.TRACE)).toBe(2); - }); - - it('compares log levels to DEBUG', () => { - expect(compareLogLevels(LogLevel.DEBUG, LogLevel.ERROR)).toBe(-3); - expect(compareLogLevels(LogLevel.DEBUG, LogLevel.WARN)).toBe(-2); - expect(compareLogLevels(LogLevel.DEBUG, LogLevel.INFO)).toBe(-1); - expect(compareLogLevels(LogLevel.DEBUG, LogLevel.DEBUG)).toBe(0); - expect(compareLogLevels(LogLevel.DEBUG, LogLevel.TRACE)).toBe(1); - }); - - it('compares log levels to TRACE', () => { - expect(compareLogLevels(LogLevel.TRACE, LogLevel.ERROR)).toBe(-4); - expect(compareLogLevels(LogLevel.TRACE, LogLevel.WARN)).toBe(-3); - expect(compareLogLevels(LogLevel.TRACE, LogLevel.INFO)).toBe(-2); - expect(compareLogLevels(LogLevel.TRACE, LogLevel.DEBUG)).toBe(-1); - expect(compareLogLevels(LogLevel.TRACE, LogLevel.TRACE)).toBe(0); - }); - - it('returns NaN if log levels are not found', () => { - expect(compareLogLevels('foo' as LogLevel, LogLevel.ERROR)).toBe(NaN); - expect(compareLogLevels(LogLevel.ERROR, 'foo' as LogLevel)).toBe(NaN); - }); -}); diff --git a/electron/common/logger/level/__tests__/get-log-level.test.ts b/electron/common/logger/level/__tests__/get-log-level.test.ts deleted file mode 100644 index d00dbe7e..00000000 --- a/electron/common/logger/level/__tests__/get-log-level.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { LogLevel } from '../../types.js'; -import { getLogLevel } from '../get-log-level.js'; - -describe('get-log-level', () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('gets the log level from the environment', () => { - vi.stubEnv('LOG_LEVEL', 'error'); - - expect(getLogLevel()).toBe(LogLevel.ERROR); - }); - - it('defaults to INFO if no log level is set', () => { - vi.stubEnv('LOG_LEVEL', ''); - - expect(getLogLevel()).toBe(LogLevel.INFO); - }); -}); diff --git a/electron/common/logger/level/__tests__/is-log-level-enabled.test.ts b/electron/common/logger/level/__tests__/is-log-level-enabled.test.ts deleted file mode 100644 index 3f2782ef..00000000 --- a/electron/common/logger/level/__tests__/is-log-level-enabled.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { LogLevel } from '../../types.js'; -import { isLogLevelEnabled } from '../is-log-level-enabled.js'; - -describe('is-log-level-enabled', () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); - - it('detects available log levels when set to ERROR', () => { - vi.stubEnv('LOG_LEVEL', 'error'); - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(false); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); - }); - - it('detects available log levels when set to WARN', () => { - vi.stubEnv('LOG_LEVEL', 'warn'); - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); - }); - - it('detects available log levels when set to INFO', () => { - vi.stubEnv('LOG_LEVEL', 'info'); - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); - }); - - it('detects available log levels when set to DEBUG', () => { - vi.stubEnv('LOG_LEVEL', 'debug'); - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); - }); - - it('detects available log levels when set to TRACE', () => { - vi.stubEnv('LOG_LEVEL', 'trace'); - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(true); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(true); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(true); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(true); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(true); - }); - - it('detects available log levels when set to UNKNOWN', () => { - vi.stubEnv('LOG_LEVEL', 'unknown'); // or any unexpected value - - expect(isLogLevelEnabled(LogLevel.ERROR)).toBe(false); - expect(isLogLevelEnabled(LogLevel.WARN)).toBe(false); - expect(isLogLevelEnabled(LogLevel.INFO)).toBe(false); - expect(isLogLevelEnabled(LogLevel.DEBUG)).toBe(false); - expect(isLogLevelEnabled(LogLevel.TRACE)).toBe(false); - }); -}); diff --git a/electron/common/logger/level/__tests__/tsconfig.json b/electron/common/logger/level/__tests__/tsconfig.json deleted file mode 100644 index 105334e2..00000000 --- a/electron/common/logger/level/__tests__/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.test.json" -} diff --git a/electron/common/logger/level/compare-log-levels.ts b/electron/common/logger/level/compare-log-levels.ts deleted file mode 100644 index 24d9bb61..00000000 --- a/electron/common/logger/level/compare-log-levels.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { LogLevel } from '../types.js'; - -/** - * Returns a number indicating the relative order of the two log levels. - * If the return value is less than 0, then lhs is less than rhs. - * If the return value is greater than 0, then lhs is greater than rhs. - * If the return value is 0, then lhs is equal to rhs. - * - * Example: - * `compareLogLevels(LogLevel.ERROR, LogLevel.INFO)` returns 2, which means - * that the error log level is two levels higher than the info log level. - */ -export const compareLogLevels = (lhs: LogLevel, rhs: LogLevel): number => { - const allLogLevels = Object.values(LogLevel); - const lhsIndex = allLogLevels.indexOf(lhs); - const rhsIndex = allLogLevels.indexOf(rhs); - - // If neither log level is found then unable to compare. - if (lhsIndex < 0 || rhsIndex < 0) { - return NaN; - } - - return rhsIndex - lhsIndex; -}; diff --git a/electron/common/logger/level/get-log-level.ts b/electron/common/logger/level/get-log-level.ts deleted file mode 100644 index aafaeb64..00000000 --- a/electron/common/logger/level/get-log-level.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LogLevel } from '../types.js'; - -export const getLogLevel = (): LogLevel => { - return (process.env.LOG_LEVEL || 'info') as LogLevel; -}; diff --git a/electron/common/logger/level/is-log-level-enabled.ts b/electron/common/logger/level/is-log-level-enabled.ts deleted file mode 100644 index 4cb3ef56..00000000 --- a/electron/common/logger/level/is-log-level-enabled.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { LogLevel } from '../types.js'; -import { compareLogLevels } from './compare-log-levels.js'; -import { getLogLevel } from './get-log-level.js'; - -/** - * Returns whether the given log level is enabled. - * - * For example, if the log level were set to 'info', then - * log levels 'error', 'warn', and 'info' would be enabled, but - * log levels 'debug' and 'trace' would not. - */ -export const isLogLevelEnabled = (logLevelToCheck: LogLevel): boolean => { - const result = compareLogLevels(getLogLevel(), logLevelToCheck); - - if (isNaN(result)) { - return false; - } - - return result <= 0; -}; diff --git a/electron/common/logger/logger.ts b/electron/common/logger/logger.ts index fdadbb20..d6933a41 100644 --- a/electron/common/logger/logger.ts +++ b/electron/common/logger/logger.ts @@ -1,7 +1,5 @@ import { nextTick } from 'node:process'; -import { compareLogLevels } from './level/compare-log-levels.js'; -import { getLogLevel } from './level/get-log-level.js'; -import { isLogLevelEnabled } from './level/is-log-level-enabled.js'; +import { isLogLevelEnabled } from './logger.utils.js'; import type { LogData, LogFormatter, @@ -180,7 +178,7 @@ export class LoggerImpl implements Logger { } protected isTransportLogLevelSupported(level?: LogLevel): boolean { - return !level || compareLogLevels(getLogLevel(), level) < 0; + return !level || isLogLevelEnabled(level); } protected addTransportErrorListener(transport: LogTransport): void { diff --git a/electron/common/logger/logger.utils.ts b/electron/common/logger/logger.utils.ts new file mode 100644 index 00000000..ce080234 --- /dev/null +++ b/electron/common/logger/logger.utils.ts @@ -0,0 +1,124 @@ +import { LogLevel } from './types.js'; + +// For performance reasons since the `LOG_LEVEL` env variable is +// not meant to be volatile during runtime, we cache computed values. +// It's a trade-off to make logging as fast as possible. +interface LogLevelCache { + logLevel?: LogLevel; + levels: Record< + LogLevel, + { + enabled?: boolean; + } + >; +} + +const newLogLevelCache = (): LogLevelCache => { + return { + levels: { + [LogLevel.ERROR]: {}, + [LogLevel.WARN]: {}, + [LogLevel.INFO]: {}, + [LogLevel.DEBUG]: {}, + [LogLevel.TRACE]: {}, + }, + }; +}; + +let logLevelCache = newLogLevelCache(); + +/** + * Exposed literally so that I can clear it in tests. + * Otherwise, there is no reason to call this method. + */ +export const clearLogLevelCache = (): void => { + logLevelCache = newLogLevelCache(); +}; + +/** + * Gets and caches the log level from the environment variable LOG_LEVEL. + * To skip the cache, use {@link computeLogLevel}. + */ +export const getLogLevel = (): LogLevel => { + if (logLevelCache.logLevel === undefined) { + logLevelCache.logLevel = computeLogLevel(); + } + return logLevelCache.logLevel; +}; + +/** + * Computes the log level from the environment variable LOG_LEVEL. + * Default is LogLevel.INFO. + * Does not cache the return value. + */ +export const computeLogLevel = (): LogLevel => { + const LogLevelMap: Partial> = { + error: LogLevel.ERROR, + warn: LogLevel.WARN, + info: LogLevel.INFO, + debug: LogLevel.DEBUG, + trace: LogLevel.TRACE, + }; + const levelStr = process.env.LOG_LEVEL?.toLowerCase(); + return LogLevelMap[levelStr || LogLevel.INFO] ?? LogLevel.INFO; +}; + +/** + * Gets and caches whether the given log level is enabled. + * To skip the cache, use {@link computeIsLogLevelEnabled}. + */ +export const isLogLevelEnabled = (logLevelToCheck: LogLevel): boolean => { + if (logLevelCache.levels[logLevelToCheck].enabled === undefined) { + const isEnabled = computeIsLogLevelEnabled(logLevelToCheck); + logLevelCache.levels[logLevelToCheck].enabled = isEnabled; + } + return logLevelCache.levels[logLevelToCheck].enabled; +}; + +/** + * Computes whether the given log level is enabled. + * Does not cache the return value. + * + * For example, if the log level were set to 'info', then + * log levels 'error', 'warn', and 'info' would be enabled, but + * log levels 'debug' and 'trace' would not. + */ +export const computeIsLogLevelEnabled = (logLevel: LogLevel): boolean => { + const result = compareLogLevels(getLogLevel(), logLevel); + + if (isNaN(result)) { + return false; + } + + return result <= 0; +}; + +/** + * Returns a number indicating the relative order of the two log levels. + * If the return value is less than 0, then lhs is more restrictive than rhs. + * If the return value is greater than 0, then lhs is less restrictive than rhs. + * If the return value is 0, then lhs is equal to rhs. + * + * Example: + * `compareLogLevels(LogLevel.ERROR, LogLevel.INFO)` returns 2, which means + * that the error log level is two levels more restrictive than info. + */ +export const compareLogLevels = (lhs: LogLevel, rhs: LogLevel): number => { + const LogLevelOrder: Record = { + [LogLevel.ERROR]: 0, + [LogLevel.WARN]: 1, + [LogLevel.INFO]: 2, + [LogLevel.DEBUG]: 3, + [LogLevel.TRACE]: 4, + }; + + const lhsIndex = LogLevelOrder[lhs]; + const rhsIndex = LogLevelOrder[rhs]; + + // If neither log level is found then unable to compare. + if (lhsIndex === undefined || rhsIndex === undefined) { + return NaN; + } + + return rhsIndex - lhsIndex; +}; diff --git a/electron/common/string/__tests__/equals-ignore-case.test.ts b/electron/common/string/__tests__/equals-ignore-case.test.ts deleted file mode 100644 index d524051d..00000000 --- a/electron/common/string/__tests__/equals-ignore-case.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { equalsIgnoreCase } from '../equals-ignore-case.js'; - -describe('equals-ignore-case', () => { - it('returns true when the values are equal', () => { - const value1 = 'foo'; - const value2 = 'foo'; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(true); - }); - - it('returns false when the values are not equal', () => { - const value1 = 'foo'; - const value2 = 'bar'; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(false); - }); - - it('returns true when the values are equal but with different casing', () => { - const value1 = 'foo'; - const value2 = 'FOO'; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(true); - }); - - it('returns false when first value is undefined', () => { - const value1 = undefined; - const value2 = 'foo'; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(false); - }); - - it('returns false when second value is undefined', () => { - const value1 = 'foo'; - const value2 = undefined; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(false); - }); - - it('returns true when both values are undefined', () => { - const value1 = undefined; - const value2 = undefined; - - const result = equalsIgnoreCase(value1, value2); - - expect(result).toEqual(true); - }); -}); diff --git a/electron/common/string/__tests__/includes-ignore-case.test.ts b/electron/common/string/__tests__/includes-ignore-case.test.ts deleted file mode 100644 index f8e71228..00000000 --- a/electron/common/string/__tests__/includes-ignore-case.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { includesIgnoreCase } from '../includes-ignore-case.js'; - -describe('includes-ignore-case', () => { - it('returns true when the value is included in the array', () => { - const values = ['foo', 'bar', 'baz']; - const valueToFind = 'bar'; - - const result = includesIgnoreCase(values, valueToFind); - - expect(result).toEqual(true); - }); - - it('returns false when the value is not included in the array', () => { - const values = ['foo', 'bar', 'baz']; - const valueToFind = 'qux'; - - const result = includesIgnoreCase(values, valueToFind); - - expect(result).toEqual(false); - }); - - it('returns true when value is included in the array but with different casing', () => { - const values = ['foo', 'bar', 'baz']; - const valueToFind = 'BAR'; - - const result = includesIgnoreCase(values, valueToFind); - - expect(result).toEqual(true); - }); - - it('returns false when value is undefined', () => { - const values = ['foo', 'bar', 'baz']; - const valueToFind = undefined; - - const result = includesIgnoreCase(values, valueToFind); - - expect(result).toEqual(false); - }); -}); diff --git a/electron/common/string/__tests__/is-blank.test.ts b/electron/common/string/__tests__/is-blank.test.ts deleted file mode 100644 index 3cd21b91..00000000 --- a/electron/common/string/__tests__/is-blank.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { isBlank } from '../is-blank.js'; - -describe('is-blank', () => { - it.each([undefined, null, '', ' ', '\n'])( - 'returns true when string is `%s`q', - async (text: null | undefined | string) => { - expect(isBlank(text)).toBe(true); - } - ); - - it.each(['a', ' a', 'a ', ' a '])( - 'returns false when string is `%s`', - async (text: string) => { - expect(isBlank(text)).toBe(false); - } - ); -}); diff --git a/electron/common/string/__tests__/is-empty.test.ts b/electron/common/string/__tests__/is-empty.test.ts deleted file mode 100644 index f575ef95..00000000 --- a/electron/common/string/__tests__/is-empty.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { isEmpty } from '../is-empty.js'; - -describe('is-empty', () => { - it.each([undefined, null, ''])( - 'returns true when string is `%s`', - async (text: null | undefined | string) => { - expect(isEmpty(text)).toBe(true); - } - ); - - it.each(['a', ' a', 'a ', ' a ', ' ', '\n'])( - 'returns false when string is `%s`', - async (text: string) => { - expect(isEmpty(text)).toBe(false); - } - ); -}); diff --git a/electron/common/string/__tests__/slice-start.test.ts b/electron/common/string/__tests__/slice-start.test.ts deleted file mode 100644 index e0710cb7..00000000 --- a/electron/common/string/__tests__/slice-start.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { sliceStart } from '../slice-start.js'; - -describe('slice-start', () => { - it('returns the original string, the matched pattern, and the remaining string when the pattern is found at the start of the string', () => { - const text = 'foo bar baz'; - const regex = /^(foo)/; - - const result = sliceStart({ text, regex }); - - expect(result).toEqual({ - match: 'foo', - original: 'foo bar baz', - remaining: ' bar baz', - }); - }); - - it('returns the original string, undefined for the matched pattern, and the original string for the remaining string when the pattern is not found at the start of the string', () => { - const text = 'foo bar baz'; - const regex = /^(bar)/; - - const result = sliceStart({ text, regex }); - - expect(result).toEqual({ - match: undefined, - original: 'foo bar baz', - remaining: 'foo bar baz', - }); - }); -}); diff --git a/electron/common/string/__tests__/string-utils.test.ts b/electron/common/string/__tests__/string-utils.test.ts new file mode 100644 index 00000000..53f27fa1 --- /dev/null +++ b/electron/common/string/__tests__/string-utils.test.ts @@ -0,0 +1,213 @@ +import { describe, expect, it } from 'vitest'; +import { + equalsIgnoreCase, + includesIgnoreCase, + isBlank, + isEmpty, + sliceStart, + toUpperSnakeCase, + unescapeEntities, +} from '../string.utils.js'; + +describe('string-utils', () => { + describe('#unescapeEntities', () => { + it('unescapes HTML entities in the text', () => { + const text = '<div>Hello, &world!</div>'; + const expected = '
Hello, &world!
'; + const result = unescapeEntities(text); + expect(result).toEqual(expected); + }); + + it('unescapes custom entities', () => { + const text = '&customEntity1; &customEntity2;'; + const options = { + entities: { + customEntity1: 'Custom 1', + customEntity2: 'Custom 2', + }, + }; + const expected = 'Custom 1 Custom 2'; + const result = unescapeEntities(text, options); + expect(result).toEqual(expected); + }); + + it('does not unescape unknown entities', () => { + const text = '&unknownEntity;'; + const expected = '&unknownEntity;'; + const result = unescapeEntities(text); + expect(result).toEqual(expected); + }); + }); + + describe('#equalsIgnoreCase', () => { + it('returns true when the values are equal', () => { + const value1 = 'foo'; + const value2 = 'foo'; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(true); + }); + + it('returns false when the values are not equal', () => { + const value1 = 'foo'; + const value2 = 'bar'; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(false); + }); + + it('returns true when the values are equal but with different casing', () => { + const value1 = 'foo'; + const value2 = 'FOO'; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(true); + }); + + it('returns false when first value is undefined', () => { + const value1 = undefined; + const value2 = 'foo'; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(false); + }); + + it('returns false when second value is undefined', () => { + const value1 = 'foo'; + const value2 = undefined; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(false); + }); + + it('returns true when both values are undefined', () => { + const value1 = undefined; + const value2 = undefined; + + const result = equalsIgnoreCase(value1, value2); + + expect(result).toEqual(true); + }); + }); + + describe('#includesIgnoreCase', () => { + it('returns true when the value is included in the array', () => { + const values = ['foo', 'bar', 'baz']; + const valueToFind = 'bar'; + + const result = includesIgnoreCase(values, valueToFind); + + expect(result).toEqual(true); + }); + + it('returns false when the value is not included in the array', () => { + const values = ['foo', 'bar', 'baz']; + const valueToFind = 'qux'; + + const result = includesIgnoreCase(values, valueToFind); + + expect(result).toEqual(false); + }); + + it('returns true when value is included in the array but with different casing', () => { + const values = ['foo', 'bar', 'baz']; + const valueToFind = 'BAR'; + + const result = includesIgnoreCase(values, valueToFind); + + expect(result).toEqual(true); + }); + + it('returns false when value is undefined', () => { + const values = ['foo', 'bar', 'baz']; + const valueToFind = undefined; + + const result = includesIgnoreCase(values, valueToFind); + + expect(result).toEqual(false); + }); + }); + + describe('#isBlank', () => { + it.each([undefined, null, '', ' ', '\n'])( + 'returns true when string is `%s`q', + async (text: null | undefined | string) => { + expect(isBlank(text)).toBe(true); + } + ); + + it.each(['a', ' a', 'a ', ' a '])( + 'returns false when string is `%s`', + async (text: string) => { + expect(isBlank(text)).toBe(false); + } + ); + }); + + describe('#isEmpty', () => { + it.each([undefined, null, ''])( + 'returns true when string is `%s`', + async (text: null | undefined | string) => { + expect(isEmpty(text)).toBe(true); + } + ); + + it.each(['a', ' a', 'a ', ' a ', ' ', '\n'])( + 'returns false when string is `%s`', + async (text: string) => { + expect(isEmpty(text)).toBe(false); + } + ); + }); + + describe('#toUpperSnakeCase', () => { + it('returns the value when the value is not camel case', () => { + const value = 'foo'; + + const result = toUpperSnakeCase(value); + + expect(result).toEqual('FOO'); + }); + + it('returns the value in upper snake case when the value is camel case', () => { + const value = 'fooBarBaz'; + + const result = toUpperSnakeCase(value); + + expect(result).toEqual('FOO_BAR_BAZ'); + }); + }); + + describe('#sliceStart', () => { + it('returns the original string, the matched pattern, and the remaining string when the pattern is found at the start of the string', () => { + const text = 'foo bar baz'; + const regex = /^(foo)/; + + const result = sliceStart({ text, regex }); + + expect(result).toEqual({ + match: 'foo', + original: 'foo bar baz', + remaining: ' bar baz', + }); + }); + + it('returns the original string, undefined for the matched pattern, and the original string for the remaining string when the pattern is not found at the start of the string', () => { + const text = 'foo bar baz'; + const regex = /^(bar)/; + + const result = sliceStart({ text, regex }); + + expect(result).toEqual({ + match: undefined, + original: 'foo bar baz', + remaining: 'foo bar baz', + }); + }); + }); +}); diff --git a/electron/common/string/__tests__/to-upper-snake-case.test.ts b/electron/common/string/__tests__/to-upper-snake-case.test.ts deleted file mode 100644 index faf4ddc5..00000000 --- a/electron/common/string/__tests__/to-upper-snake-case.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { toUpperSnakeCase } from '../to-upper-snake-case.js'; - -describe('string-utils', () => { - it('returns the value when the value is not camel case', () => { - const value = 'foo'; - - const result = toUpperSnakeCase(value); - - expect(result).toEqual('FOO'); - }); - - it('returns the value in upper snake case when the value is camel case', () => { - const value = 'fooBarBaz'; - - const result = toUpperSnakeCase(value); - - expect(result).toEqual('FOO_BAR_BAZ'); - }); -}); diff --git a/electron/common/string/__tests__/unescape-entities.test.ts b/electron/common/string/__tests__/unescape-entities.test.ts deleted file mode 100644 index e14cec90..00000000 --- a/electron/common/string/__tests__/unescape-entities.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { unescapeEntities } from '../unescape-entities.js'; - -describe('unescape-entities', () => { - it('unescapes HTML entities in the text', () => { - const text = '<div>Hello, &world!</div>'; - const expected = '
Hello, &world!
'; - const result = unescapeEntities(text); - expect(result).toEqual(expected); - }); - - it('unescapes custom entities', () => { - const text = '&customEntity1; &customEntity2;'; - const options = { - entities: { - customEntity1: 'Custom 1', - customEntity2: 'Custom 2', - }, - }; - const expected = 'Custom 1 Custom 2'; - const result = unescapeEntities(text, options); - expect(result).toEqual(expected); - }); - - it('does not unescape unknown entities', () => { - const text = '&unknownEntity;'; - const expected = '&unknownEntity;'; - const result = unescapeEntities(text); - expect(result).toEqual(expected); - }); -}); diff --git a/electron/common/string/equals-ignore-case.ts b/electron/common/string/equals-ignore-case.ts deleted file mode 100644 index cbc92d30..00000000 --- a/electron/common/string/equals-ignore-case.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Maybe } from '../types.js'; - -export const equalsIgnoreCase = ( - a: Maybe, - b: Maybe -): boolean => { - return a?.toLowerCase() === b?.toLowerCase(); -}; diff --git a/electron/common/string/includes-ignore-case.ts b/electron/common/string/includes-ignore-case.ts deleted file mode 100644 index 5cd7c269..00000000 --- a/electron/common/string/includes-ignore-case.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Maybe } from '../types.js'; -import { equalsIgnoreCase } from './equals-ignore-case.js'; - -export const includesIgnoreCase = ( - values: Array, - valueToFind: Maybe -): boolean => { - return values.some((value) => equalsIgnoreCase(value, valueToFind)); -}; diff --git a/electron/common/string/is-blank.ts b/electron/common/string/is-blank.ts deleted file mode 100644 index 51a401d5..00000000 --- a/electron/common/string/is-blank.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isEmpty } from './is-empty.js'; - -/** - * Returns true if the text is undefined, null, or is empty when trimmed. - * Whitespace characters are ignored. - * - * We use a type guard in result to hint that if this function returns false - * then the value cannot be null or undefined. - */ -export const isBlank = ( - text: string | null | undefined -): text is null | undefined => { - return isEmpty(text?.trim()); -}; diff --git a/electron/common/string/is-empty.ts b/electron/common/string/is-empty.ts deleted file mode 100644 index 5d2a42d5..00000000 --- a/electron/common/string/is-empty.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Returns true if the text is undefined, null, or empty string (''). - * Whitespace characters are considered non-empty. - * - * We use a type guard in result to hint that if this function returns false - * then the value cannot be null or undefined. - */ -export const isEmpty = ( - text: string | null | undefined -): text is null | undefined => { - return !text || text === ''; -}; diff --git a/electron/common/string/slice-start.ts b/electron/common/string/slice-start.ts deleted file mode 100644 index 44209299..00000000 --- a/electron/common/string/slice-start.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Inspired by Ruby's String#slice method. - * Slices the pattern from the start of the input text. - * Returns an object containing the matched pattern, original text, and remaining text. - */ -export const sliceStart = (options: { - /** - * The input text to slice the pattern from. - */ - text: string; - /** - * The pattern to match at the start of the input text. - * Must include the ^ anchor to match the start of the string. - * Must include one capturing group, which will be the returned matched text. - * - * Examples: - * Good: /^(.+)/ One capturing group and ^ anchor - * Bad: /^.+/ Missing capturing group - * Bad: /(.+)/ Missing ^ anchor - */ - regex: RegExp; -}): { - /** - * The first captured group matched by the pattern in the input text. - */ - match?: string; - /** - * The original input text echoed back. - */ - original: string; - /** - * The remaining text after the matched pattern. - */ - remaining: string; -} => { - const { text, regex } = options; - - // If a pattern is found, the result will be an array; otherwise, null. - // The first element of the array will be the matched text. - const matchResult = text.match(regex); - - if (matchResult) { - // The matched text is everything the regex pattern matched, - // which may be more than what the capturing groups matched. - // The captured text is only what was in the first captured group. - const [matchedText, capturedText] = matchResult; - const original = text; - const remaining = text.slice(matchedText.length); - - return { - match: capturedText, - original, - remaining, - }; - } - - // No match, so no change. - return { - match: undefined, - original: text, - remaining: text, - }; -}; diff --git a/electron/common/string/string.utils.ts b/electron/common/string/string.utils.ts new file mode 100644 index 00000000..b0c6fe00 --- /dev/null +++ b/electron/common/string/string.utils.ts @@ -0,0 +1,168 @@ +import snakeCase from 'lodash-es/snakeCase.js'; +import type { Maybe } from '../types.js'; + +/** + * Map of XML entities to their unescaped values. + */ +const UNESCAPABLE_ENTITIES: Record = { + lt: '<', + gt: '>', + quot: '"', + apos: "'", + amp: '&', +}; + +const UNESCAPE_ENTITIES_REGEX = /&([a-zA-Z0-9]+);/g; + +/** + * Unescapes XML entities. + * For example, converts '<' to '<'. + * + * By default, unescapes the following entities: + * - < + * - > + * - " + * - ' + * - & + */ +export const unescapeEntities = ( + text: string, + options?: { + /** + * Specify your own entities to unescape. + * The keys should be the entity name like 'lt' or 'gt'. + * The values should be the unescaped value like '<' or '>'. + */ + entities?: Record; + } +): string => { + const { entities = UNESCAPABLE_ENTITIES } = options ?? {}; + + // Replaces the matched text with the return value of the callback function. + // The capturing group just helps us identify which entity to unescape. + return text.replace( + UNESCAPE_ENTITIES_REGEX, + (matchedText, capturedText, _index, _allText) => { + return entities[capturedText] ?? matchedText; + } + ); +}; + +/** + * Returns true if the two strings are equal, ignoring case. + */ +export const equalsIgnoreCase = ( + a: Maybe, + b: Maybe +): boolean => { + return a?.toLowerCase() === b?.toLowerCase(); +}; + +/** + * Like `Array.prototype.includes`, but ignores case. + */ +export const includesIgnoreCase = ( + values: Array, + valueToFind: Maybe +): boolean => { + return values.some((value) => equalsIgnoreCase(value, valueToFind)); +}; + +/** + * Returns true if the text is undefined, null, or is empty when trimmed. + * Whitespace characters are ignored. + * + * We use a type guard in result to hint that if this function returns false + * then the value cannot be null or undefined. + */ +export const isBlank = ( + text: string | null | undefined +): text is null | undefined => { + return isEmpty(text?.trim()); +}; + +/** + * Returns true if the text is undefined, null, or empty string (''). + * Whitespace characters are considered non-empty. + * + * We use a type guard in result to hint that if this function returns false + * then the value cannot be null or undefined. + */ +export const isEmpty = ( + text: string | null | undefined +): text is null | undefined => { + return !text || text === ''; +}; + +/** + * Returns the text, uppercased and converted to snake_case. + * + * For example, 'fooBarBaz' becomes 'FOO_BAR_BAZ'. + */ +export const toUpperSnakeCase = (value: string): string => { + return snakeCase(value).toUpperCase(); +}; + +/** + * Inspired by Ruby's String#slice method. + * Slices the pattern from the start of the input text. + * Returns an object containing the matched pattern, original text, and remaining text. + */ +export const sliceStart = (options: { + /** + * The input text to slice the pattern from. + */ + text: string; + /** + * The pattern to match at the start of the input text. + * Must include the ^ anchor to match the start of the string. + * Must include one capturing group, which will be the returned matched text. + * + * Examples: + * Good: /^(.+)/ One capturing group and ^ anchor + * Bad: /^.+/ Missing capturing group + * Bad: /(.+)/ Missing ^ anchor + */ + regex: RegExp; +}): { + /** + * The first captured group matched by the pattern in the input text. + */ + match?: string; + /** + * The original input text echoed back. + */ + original: string; + /** + * The remaining text after the matched pattern. + */ + remaining: string; +} => { + const { text, regex } = options; + + // If a pattern is found, the result will be an array; otherwise, null. + // The first element of the array will be the matched text. + const matchResult = text.match(regex); + + if (matchResult) { + // The matched text is everything the regex pattern matched, + // which may be more than what the capturing groups matched. + // The captured text is only what was in the first captured group. + const [matchedText, capturedText] = matchResult; + const original = text; + const remaining = text.slice(matchedText.length); + + return { + match: capturedText, + original, + remaining, + }; + } + + // No match, so no change. + return { + match: undefined, + original: text, + remaining: text, + }; +}; diff --git a/electron/common/string/to-upper-snake-case.ts b/electron/common/string/to-upper-snake-case.ts deleted file mode 100644 index b19521b9..00000000 --- a/electron/common/string/to-upper-snake-case.ts +++ /dev/null @@ -1,5 +0,0 @@ -import snakeCase from 'lodash-es/snakeCase.js'; - -export const toUpperSnakeCase = (value: string): string => { - return snakeCase(value).toUpperCase(); -}; diff --git a/electron/common/string/unescape-entities.ts b/electron/common/string/unescape-entities.ts deleted file mode 100644 index ff0f4d64..00000000 --- a/electron/common/string/unescape-entities.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Map of XML entities to their unescaped values. - */ -const UNESCAPABLE_ENTITIES: Record = { - lt: '<', - gt: '>', - quot: '"', - apos: "'", - amp: '&', -}; - -const UNESCAPE_ENTITIES_REGEX = /&([a-zA-Z0-9]+);/g; - -/** - * Unescapes XML entities. - * For example, converts '<' to '<'. - * - * By default, unescapes the following entities: - * - < - * - > - * - " - * - ' - * - & - */ -export const unescapeEntities = ( - text: string, - options?: { - /** - * Specify your own entities to unescape. - * The keys should be the entity name like 'lt' or 'gt'. - * The values should be the unescaped value like '<' or '>'. - */ - entities?: Record; - } -): string => { - const { entities = UNESCAPABLE_ENTITIES } = options ?? {}; - - // Replaces the matched text with the return value of the callback function. - // The capturing group just helps us identify which entity to unescape. - return text.replace( - UNESCAPE_ENTITIES_REGEX, - (matchedText, capturedText, _index, _allText) => { - return entities[capturedText] ?? matchedText; - } - ); -};