Skip to content

Commit 2edb628

Browse files
committed
implement 'camelcaseKeys' function
1 parent 29bf7ac commit 2edb628

File tree

2 files changed

+220
-8
lines changed

2 files changed

+220
-8
lines changed

src/utils.spec.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { snakeToCamelCase, camelizeResponse } from './utils';
2+
3+
describe('src/Utils', () => {
4+
describe('snakeToCamelCase()', () => {
5+
it('correctly converts snake_case to camelCase', () => {
6+
const cases = [
7+
{ input: 'snake_case', expected: 'snakeCase' },
8+
{ input: 'camel_case_party_time', expected: 'camelCasePartyTime' },
9+
{
10+
input: 'really_long_snake_case_input_string_time',
11+
expected: 'reallyLongSnakeCaseInputStringTime',
12+
},
13+
];
14+
cases.forEach((testCase) => {
15+
expect(snakeToCamelCase(testCase.input)).toEqual(testCase.expected);
16+
});
17+
});
18+
19+
it('correctly handles double underscore, leading or trailing underscore', () => {
20+
const cases = [
21+
{ input: 'snake__case', expected: 'snakeCase' },
22+
{ input: '_camel_case_party_time', expected: 'camelCasePartyTime' },
23+
{
24+
input: 'really_long_snake_case_input_string_time_',
25+
expected: 'reallyLongSnakeCaseInputStringTime',
26+
},
27+
];
28+
cases.forEach((testCase) => {
29+
expect(snakeToCamelCase(testCase.input)).toEqual(testCase.expected);
30+
});
31+
});
32+
33+
it('returns strings containing "-" unchanged', () => {
34+
const cases = [
35+
{ input: 'snake-case', expected: 'snake-case' },
36+
{ input: 'camel-case-party-time', expected: 'camel-case-party-time' },
37+
{
38+
input: 'en-GB',
39+
expected: 'en-GB',
40+
},
41+
];
42+
cases.forEach((testCase) => {
43+
expect(snakeToCamelCase(testCase.input)).toEqual(testCase.expected);
44+
});
45+
});
46+
47+
it('preserves existing camel casing', () => {
48+
const cases = [
49+
{ input: '_existingCamelCase', expected: 'existingCamelCase' },
50+
{ input: 'camelCase_party_time', expected: 'camelCasePartyTime' },
51+
{
52+
input: 'mixed_caseString_example',
53+
expected: 'mixedCaseStringExample',
54+
},
55+
];
56+
cases.forEach((testCase) => {
57+
expect(snakeToCamelCase(testCase.input)).toEqual(testCase.expected);
58+
});
59+
});
60+
});
61+
describe('camelcaseKeys()', () => {
62+
it("converts an object's keys from snake_case to camelCase", () => {
63+
const cases = [
64+
{ input: { snake_case: 1 }, expected: { snakeCase: 1 } },
65+
{
66+
input: { snake_case: 1, another_snake_case: 2 },
67+
expected: { snakeCase: 1, anotherSnakeCase: 2 },
68+
},
69+
];
70+
cases.forEach((testCase) => {
71+
expect(camelizeResponse(testCase.input)).toEqual(testCase.expected);
72+
});
73+
});
74+
75+
it("converts a nested object's keys from snake_case to camelCase", () => {
76+
const cases = [
77+
{
78+
input: { snake_case_: { ab_cd: 1 } },
79+
expected: { snakeCase: { abCd: 1 } },
80+
},
81+
{
82+
input: { snake_case: { 'en-GB': 1 } },
83+
expected: { snakeCase: { 'en-GB': 1 } },
84+
},
85+
{
86+
input: { 'en-GB': { _snake_case_plus: { another__one: 1 } } },
87+
expected: { 'en-GB': { snakeCasePlus: { anotherOne: 1 } } },
88+
},
89+
{
90+
input: {
91+
'en-GB': { _snake_case_plus: { another__one: { yet_another: 1 } } },
92+
},
93+
expected: {
94+
'en-GB': { snakeCasePlus: { anotherOne: { yetAnother: 1 } } },
95+
},
96+
},
97+
];
98+
cases.forEach((testCase) => {
99+
expect(camelizeResponse(testCase.input)).toEqual(testCase.expected);
100+
});
101+
});
102+
103+
it('converts keys of objects contained in an array', () => {
104+
const cases = [
105+
{
106+
input: { snake_case_: { ab_cd: [{ qwerty_dvorak: 1 }] } },
107+
expected: { snakeCase: { abCd: [{ qwertyDvorak: 1 }] } },
108+
},
109+
{
110+
input: {
111+
snake_case_: [
112+
{ ab_cd: [{ qwerty_dvorak: 1 }] },
113+
'dont_change_me',
114+
{ zxy_wt: [7, { _pp: 42, llll_mmm: 'aa_aa' }] },
115+
],
116+
case_snake: {
117+
_snake_camel: [
118+
{ mn_op_qr_st: [[{ obj_key: ['a', 7, { key_obj_: 99 }] }]] },
119+
],
120+
},
121+
},
122+
expected: {
123+
snakeCase: [
124+
{ abCd: [{ qwertyDvorak: 1 }] },
125+
'dont_change_me',
126+
{ zxyWt: [7, { pp: 42, llllMmm: 'aa_aa' }] },
127+
],
128+
caseSnake: {
129+
snakeCamel: [
130+
{ mnOpQrSt: [[{ objKey: ['a', 7, { keyObj: 99 }] }]] },
131+
],
132+
},
133+
},
134+
},
135+
{
136+
input: [
137+
42,
138+
{ snake_case_: { ab_cd: [{ qwerty_dvorak: 1 }] } },
139+
'180',
140+
],
141+
expected: [42, { snakeCase: { abCd: [{ qwertyDvorak: 1 }] } }, '180'],
142+
},
143+
];
144+
cases.forEach((testCase) => {
145+
expect(camelizeResponse(testCase.input)).toEqual(testCase.expected);
146+
});
147+
});
148+
});
149+
});

src/utils.ts

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,71 @@
1-
import camelcaseKeys from 'camelcase-keys';
2-
3-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4-
export const camelizeResponse = (response: any) =>
5-
camelcaseKeys(response, {
6-
deep: true,
7-
exclude: [/-/],
8-
});
1+
/**
2+
* Converts snake_case to camelCase
3+
* @param {string} input - snake_case string
4+
* @param {string} exclude - `input` that contains `exclude` will be returned unmodified
5+
* @returns {string} - camelCase string
6+
*/
7+
export function snakeToCamelCase(input: string, exclude = '-'): string {
8+
// Return the input string unchanged if there are no underscores
9+
// or it includes the `exclude` character.
10+
if (!input.includes('_') || input.includes(exclude)) {
11+
return input;
12+
}
13+
// remove trailing/leading underscores
14+
let output = input.replace(/^_+/, '').replace(/_+$/, '');
15+
let currentIndex = output.indexOf('_');
16+
while (currentIndex !== -1) {
17+
output =
18+
output.slice(0, currentIndex) +
19+
output[currentIndex + 1].toUpperCase() +
20+
output.slice(currentIndex + 2);
21+
currentIndex = output.indexOf('_', currentIndex);
22+
}
23+
return output;
24+
}
25+
26+
const isObject = (value: unknown) =>
27+
typeof value === 'object' &&
28+
value !== null &&
29+
!Array.isArray(value) &&
30+
!(value instanceof RegExp) &&
31+
!(value instanceof Error) &&
32+
!(value instanceof Date);
33+
34+
const processArray = (arr: Array<unknown>): unknown[] =>
35+
arr.map((el) =>
36+
Array.isArray(el)
37+
? processArray(el)
38+
: isObject(el)
39+
? camelizeResponse(el as Record<string, unknown>)
40+
: el
41+
);
42+
43+
/**
44+
* Deeply clones an object and converts keys from snake_case to camelCase
45+
* @param input - object with some snake_case keys
46+
* @returns - object with camelCase keys
47+
*/
48+
export function camelizeResponse(input: unknown): unknown {
49+
if (!input) {
50+
return input;
51+
}
52+
if (Array.isArray(input)) {
53+
return processArray(input);
54+
}
55+
56+
const output: Record<string, unknown> = {};
57+
58+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
59+
if (Array.isArray(value)) {
60+
output[snakeToCamelCase(key)] = processArray(value);
61+
} else if (isObject(value)) {
62+
output[snakeToCamelCase(key)] = camelizeResponse(
63+
value as Record<string, unknown>
64+
);
65+
} else {
66+
output[snakeToCamelCase(key)] = value;
67+
}
68+
}
69+
70+
return output;
71+
}

0 commit comments

Comments
 (0)