Skip to content

Commit 8172773

Browse files
authored
Merge pull request #900 from maxmind/jpoole/replace-camelcasekeys
Remove camelcase-keys dependency
2 parents 29bf7ac + e494dd8 commit 8172773

File tree

6 files changed

+212
-26
lines changed

6 files changed

+212
-26
lines changed

package-lock.json

Lines changed: 4 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
},
6969
"dependencies": {
7070
"@maxmind/geoip2-node": "^3.0.0",
71-
"camelcase-keys": "^7.0.0",
7271
"maxmind": "^4.1.0",
7372
"punycode": "^2.1.1",
7473
"snakecase-keys": "^5.1.2",

src/response/models/insights.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ export default class Insights extends Score {
5858
response: webRecords.InsightsResponse,
5959
prop: keyof webRecords.InsightsResponse
6060
): T | undefined {
61-
return response[prop]
62-
? (camelizeResponse(response[prop]) as unknown as T)
63-
: undefined;
61+
return response[prop] ? (camelizeResponse(response[prop]) as T) : undefined;
6462
}
6563

6664
private getIpAddress(

src/response/models/score.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default class Score {
5151
this.queriesRemaining = response.queries_remaining;
5252
this.riskScore = response.risk_score;
5353
this.warnings = response.warnings
54-
? (camelizeResponse(response.warnings) as unknown as records.Warning[])
54+
? (camelizeResponse(response.warnings) as records.Warning[])
5555
: undefined;
5656
}
5757
}

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: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,57 @@
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+
* @returns {string} - camelCase string
5+
*/
6+
export function snakeToCamelCase(input: string): string {
7+
return input.replace(/_+(\w?)/g, (_, p, o) =>
8+
o === 0 ? p : p.toUpperCase()
9+
);
10+
}
11+
12+
const isObject = (value: unknown) =>
13+
typeof value === 'object' &&
14+
value !== null &&
15+
!Array.isArray(value) &&
16+
!(value instanceof RegExp) &&
17+
!(value instanceof Error) &&
18+
!(value instanceof Date);
19+
20+
const processArray = (arr: Array<unknown>): unknown[] =>
21+
arr.map((el) =>
22+
Array.isArray(el)
23+
? processArray(el)
24+
: isObject(el)
25+
? camelizeResponse(el as Record<string, unknown>)
26+
: el
27+
);
28+
29+
/**
30+
* Deeply clones an object and converts keys from snake_case to camelCase
31+
* @param input - object with some snake_case keys
32+
* @returns - object with camelCase keys
33+
*/
34+
export function camelizeResponse(input: unknown): unknown {
35+
if (!input) {
36+
return input;
37+
}
38+
if (Array.isArray(input)) {
39+
return processArray(input);
40+
}
41+
42+
const output: Record<string, unknown> = {};
43+
44+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
45+
if (Array.isArray(value)) {
46+
output[snakeToCamelCase(key)] = processArray(value);
47+
} else if (isObject(value)) {
48+
output[snakeToCamelCase(key)] = camelizeResponse(
49+
value as Record<string, unknown>
50+
);
51+
} else {
52+
output[snakeToCamelCase(key)] = value;
53+
}
54+
}
55+
56+
return output;
57+
}

0 commit comments

Comments
 (0)