diff --git a/bun.lockb b/bun.lockb index 9e5f9c3..a8f78ec 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index cd97d94..839b7cc 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@types/bun": "^1.1.13", - "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/node": "^20.16.1", "semantic-release": "^24.2.0", @@ -93,7 +92,6 @@ "dependencies": { "@prisma/client": "^5.18.0", "iovalkey": "^0.2.1", - "lodash-es": "^4.17.21", "micromatch": "^4.0.7", "object-code": "^1.3.3", "promise-coalesce": "^1.1.2" diff --git a/src/cacheKey.ts b/src/cacheKey.ts index ed88085..872c070 100644 --- a/src/cacheKey.ts +++ b/src/cacheKey.ts @@ -1,5 +1,4 @@ -import {camelCase, kebabCase, snakeCase, startCase} from 'lodash-es'; - +import { CacheCase, caseMap } from './keyCases'; import type { CacheAutoKeyParams, CacheKeyParams, @@ -11,19 +10,6 @@ const globCheckRegex = /[*?]/; const globCheck = (s: string) => globCheckRegex.test(s); -export enum CacheCase { - CAMEL_CASE = 'camelCase', - KEBAB_CASE = 'kebabCase', - SNAKE_CASE = 'snakeCase', - START_CASE = 'startCase', -} - -export const caseMap = { - [CacheCase.CAMEL_CASE]: camelCase, - [CacheCase.KEBAB_CASE]: kebabCase, - [CacheCase.SNAKE_CASE]: snakeCase, - [CacheCase.START_CASE]: startCase, -}; export const getKeyGen = ( diff --git a/src/index.ts b/src/index.ts index 1b2c29f..6f41d33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,4 @@ export type { UncacheOptions, } from './types'; export {filterOperations, unlinkPatterns} from './cacheUncache'; -export {CacheCase} from './cacheKey'; +export {CacheCase} from './keyCases'; diff --git a/src/keyCases.ts b/src/keyCases.ts new file mode 100644 index 0000000..f1fdc3e --- /dev/null +++ b/src/keyCases.ts @@ -0,0 +1,131 @@ +/** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar--'); + * // => 'fooBar' + * + * _.camelCase('__FOO_BAR__'); + * // => 'fooBar' + */ +export const camelCase = (str = ''): string => { + if (!str) return ''; + let string = str.toLowerCase(); + // Replace all special characters (hyphens, underscores) with spaces + string = string.replace(/[-_]+/g, ' '); + string = string.trim(); + // Split by spaces + const words = string.split(/\s+/); + // Capitalize all words except the first one + const camelCased = words.map((word, index) => { + if (index === 0) return word; + return word.charAt(0).toUpperCase() + word.slice(1); + }).join(''); + return camelCased; +}; + +/** + * Converts `string` to + * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). + * + * @example + * + * _.kebabCase('Foo Bar'); + * // => 'foo-bar' + * + * _.kebabCase('fooBar'); + * // => 'foo-bar' + * + * _.kebabCase('__FOO_BAR__'); + * // => 'foo-bar' + */ +export const kebabCase = (str = ''): string => { + if (!str) return ''; + // First, handle camelCase and PascalCase + let string = str.replace(/([a-z])([A-Z])/g, '$1 $2'); + string = string.toLowerCase(); + // Replace all hyphens, underscores, and special characters with spaces + string = string.replace(/[_\-]+/g, ' '); + // Replace all remaining non-alphanumeric characters with spaces + string = string.replace(/[^\w\s]/g, ' '); + // Trim the string and replace spaces with hyphens + string = string.trim().replace(/\s+/g, '-'); + return string; +}; + +/** + * Converts `string` to + * [snake case](https://en.wikipedia.org/wiki/Snake_case). + * + * @example + * + * _.snakeCase('Foo Bar'); + * // => 'foo_bar' + * + * _.snakeCase('fooBar'); + * // => 'foo_bar' + * + * _.snakeCase('--FOO-BAR--'); + * // => 'foo_bar' + */ +export const snakeCase = (str = ''): string => { + if (!str) return ''; + // First, handle camelCase and PascalCase + let string = str.replace(/([a-z])([A-Z])/g, '$1 $2'); + string = string.toLowerCase(); + // Replace all hyphens, underscores, and special characters with spaces + string = string.replace(/[_\-]+/g, ' '); + // Replace all remaining non-alphanumeric characters with spaces + string = string.replace(/[^\w\s]/g, ' '); + // Trim the string and replace spaces with underscores + string = string.trim().replace(/\s+/g, '_'); + return string; +}; + +/** + * Converts `string` to + * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). + * + * @example + * + * _.startCase('--foo-bar--'); + * // => 'Foo Bar' + * + * _.startCase('fooBar'); + * // => 'Foo Bar' + * + * _.startCase('__FOO_BAR__'); + * // => 'FOO BAR' + */ +export const startCase = (str = ''): string => { + if (!str) return ''; + // First, handle camelCase and PascalCase + let string = str.replace(/([a-z])([A-Z])/g, '$1 $2'); + // Replace all hyphens, underscores, and special characters with spaces + string = string.replace(/[_\-]+/g, ' '); + // Replace all remaining non-alphanumeric characters with spaces + string = string.replace(/[^\w\s]/g, ' '); + // Trim the string, split by spaces, capitalize each word, and join back with spaces + string = string.trim().split(/\s+/).map(word => { + return word.charAt(0).toUpperCase() + word.slice(1); + }).join(' '); + return string; +}; + +export enum CacheCase { + CAMEL_CASE = 'camelCase', + KEBAB_CASE = 'kebabCase', + SNAKE_CASE = 'snakeCase', + START_CASE = 'startCase', +} + +export const caseMap = { + [CacheCase.CAMEL_CASE]: camelCase, + [CacheCase.KEBAB_CASE]: kebabCase, + [CacheCase.SNAKE_CASE]: snakeCase, + [CacheCase.START_CASE]: startCase, +}; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 3ff517e..396aa9c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import type { } from '@prisma/client/runtime/library'; import type {Redis, RedisOptions} from 'iovalkey'; -import type {CacheCase} from './cacheKey'; +import type {CacheCase} from './keyCases'; export const ALL_OPERATIONS = [ '$executeRaw', diff --git a/test/unit/key-cases.test.ts b/test/unit/key-cases.test.ts new file mode 100644 index 0000000..22bf607 --- /dev/null +++ b/test/unit/key-cases.test.ts @@ -0,0 +1,126 @@ +import {expect, test} from 'bun:test'; +import {camelCase, kebabCase, snakeCase, startCase} from '../../src/keyCases'; + +// Tests for camelCase +test('camelCase: should handle regular space-separated words', () => { + expect(camelCase('Foo Bar')).toBe('fooBar'); +}); + +test('camelCase: should handle hyphenated words', () => { + expect(camelCase('--foo-bar--')).toBe('fooBar'); +}); + +test('camelCase: should handle underscore separated words', () => { + expect(camelCase('__FOO_BAR__')).toBe('fooBar'); +}); + +test('camelCase: should handle empty string', () => { + expect(camelCase('')).toBe(''); +}); + +test('camelCase: should handle undefined', () => { + expect(camelCase(undefined)).toBe(''); +}); + +test('camelCase: should handle mixed delimiters', () => { + expect(camelCase('foo_bar-baz')).toBe('fooBarBaz'); +}); + +// Tests for kebabCase +test('kebabCase: should handle regular space-separated words', () => { + expect(kebabCase('Foo Bar')).toBe('foo-bar'); +}); + +test('kebabCase: should handle camelCase words', () => { + expect(kebabCase('fooBar')).toBe('foo-bar'); +}); + +test('kebabCase: should handle PascalCase words', () => { + expect(kebabCase('FooBar')).toBe('foo-bar'); +}); + +test('kebabCase: should handle special characters', () => { + expect(kebabCase('__FOO_BAR__')).toBe('foo-bar'); +}); + +test('kebabCase: should handle empty string', () => { + expect(kebabCase('')).toBe(''); +}); + +test('kebabCase: should handle undefined', () => { + expect(kebabCase(undefined)).toBe(''); +}); + +test('kebabCase: should handle mixed delimiters', () => { + expect(kebabCase('foo_bar-baz')).toBe('foo-bar-baz'); +}); + +test('kebabCase: should handle already kebab cased strings', () => { + expect(kebabCase('foo-bar')).toBe('foo-bar'); +}); + +// Tests for snakeCase +test('snakeCase: should handle regular space-separated words', () => { + expect(snakeCase('Foo Bar')).toBe('foo_bar'); +}); + +test('snakeCase: should handle camelCase words', () => { + expect(snakeCase('fooBar')).toBe('foo_bar'); +}); + +test('snakeCase: should handle PascalCase words', () => { + expect(snakeCase('FooBar')).toBe('foo_bar'); +}); + +test('snakeCase: should handle hyphenated words', () => { + expect(snakeCase('--FOO-BAR--')).toBe('foo_bar'); +}); + +test('snakeCase: should handle empty string', () => { + expect(snakeCase('')).toBe(''); +}); + +test('snakeCase: should handle undefined', () => { + expect(snakeCase(undefined)).toBe(''); +}); + +test('snakeCase: should handle mixed delimiters', () => { + expect(snakeCase('foo-bar_baz')).toBe('foo_bar_baz'); +}); + +test('snakeCase: should handle already snake cased strings', () => { + expect(snakeCase('foo_bar')).toBe('foo_bar'); +}); + +// Tests for startCase +test('startCase: should handle regular space-separated words', () => { + expect(startCase('foo bar')).toBe('Foo Bar'); +}); + +test('startCase: should handle camelCase words', () => { + expect(startCase('fooBar')).toBe('Foo Bar'); +}); + +test('startCase: should handle PascalCase words', () => { + expect(startCase('FooBar')).toBe('Foo Bar'); +}); + +test('startCase: should handle hyphenated words', () => { + expect(startCase('--foo-bar--')).toBe('Foo Bar'); +}); + +test('startCase: should handle underscore words', () => { + expect(startCase('__FOO_BAR__')).toBe('FOO BAR'); +}); + +test('startCase: should handle empty string', () => { + expect(startCase('')).toBe(''); +}); + +test('startCase: should handle undefined', () => { + expect(startCase(undefined)).toBe(''); +}); + +test('startCase: should handle mixed delimiters', () => { + expect(startCase('foo-bar_baz')).toBe('Foo Bar Baz'); +}); \ No newline at end of file