Skip to content

Commit fdc8f46

Browse files
committed
Refactor case conversion utilities to use dedicated transformation functions
1 parent 85dba79 commit fdc8f46

File tree

16 files changed

+687
-171
lines changed

16 files changed

+687
-171
lines changed

packages/apps/fortune/exchange-oracle/server/src/common/interceptors/snake-case.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,26 @@ import {
66
} from '@nestjs/common';
77
import { Observable } from 'rxjs';
88
import { map } from 'rxjs/operators';
9-
import { CaseConverter } from '../utils/case-converter';
9+
import {
10+
transformKeysFromCamelToSnake,
11+
transformKeysFromSnakeToCamel,
12+
} from '../utils/case-converter';
1013

1114
@Injectable()
1215
export class SnakeCaseInterceptor implements NestInterceptor {
1316
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
1417
const request = context.switchToHttp().getRequest();
1518

1619
if (request.body) {
17-
request.body = CaseConverter.transformToCamelCase(request.body);
20+
request.body = transformKeysFromSnakeToCamel(request.body);
1821
}
1922

2023
if (request.query) {
21-
request.query = CaseConverter.transformToCamelCase(request.query);
24+
request.query = transformKeysFromSnakeToCamel(request.query);
2225
}
2326

2427
return next
2528
.handle()
26-
.pipe(map((data) => CaseConverter.transformToSnakeCase(data)));
29+
.pipe(map((data) => transformKeysFromCamelToSnake(data)));
2730
}
2831
}

packages/apps/fortune/exchange-oracle/server/src/common/pipes/validation.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ValidationPipeOptions,
66
} from '@nestjs/common';
77
import { ValidationError } from '../errors';
8-
import { CaseConverter } from '../utils/case-converter';
8+
import { camelToSnake } from '../utils/case-converter';
99

1010
@Injectable()
1111
export class HttpValidationPipe extends ValidationPipe {
@@ -32,9 +32,7 @@ export class HttpValidationPipe extends ValidationPipe {
3232

3333
const transformedPath = rawPath
3434
.split('.')
35-
.map((segment) =>
36-
segment.replace(/^[A-Za-z0-9_]+/, CaseConverter.transformToSnakeCase),
37-
)
35+
.map((segment) => segment.replace(/^[A-Za-z0-9_]+/, camelToSnake))
3836
.join('.');
3937

4038
return `${transformedPath} ${rest}`;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import * as CaseConverter from './case-converter';
4+
5+
describe('Case converting utilities', () => {
6+
describe('transformKeysFromSnakeToCamel', () => {
7+
it.each([
8+
'string',
9+
42,
10+
BigInt(0),
11+
new Date(),
12+
Symbol('test'),
13+
true,
14+
null,
15+
undefined,
16+
])('should not transform basic value [%#]', (value: unknown) => {
17+
expect(CaseConverter.transformKeysFromSnakeToCamel(value)).toEqual(value);
18+
});
19+
20+
it('should not transform simple array', () => {
21+
const input = faker.helpers.multiple(() => faker.string.sample());
22+
23+
const output = CaseConverter.transformKeysFromSnakeToCamel(input);
24+
25+
expect(output).toEqual(input);
26+
});
27+
28+
it('should transform array of objects', () => {
29+
const input = faker.helpers.multiple(() => ({
30+
test_case: faker.string.sample(),
31+
}));
32+
const expectedOutput = input.map((v) => ({
33+
testCase: v.test_case,
34+
}));
35+
36+
const output = CaseConverter.transformKeysFromSnakeToCamel(input);
37+
38+
expect(output).toEqual(expectedOutput);
39+
});
40+
41+
it('should transform plain object to camelCase', () => {
42+
const input = {
43+
random_string: faker.string.sample(),
44+
random_number: faker.number.float(),
45+
random_boolean: faker.datatype.boolean(),
46+
always_null: null,
47+
};
48+
49+
const output = CaseConverter.transformKeysFromSnakeToCamel(input);
50+
51+
expect(output).toEqual({
52+
randomString: input.random_string,
53+
randomNumber: input.random_number,
54+
randomBoolean: input.random_boolean,
55+
alwaysNull: null,
56+
});
57+
});
58+
59+
it('should transform input with nested data', () => {
60+
const randomString = faker.string.sample();
61+
62+
const input = {
63+
nested_object: {
64+
with_array: [
65+
{
66+
of_objects: {
67+
with_random_string: randomString,
68+
},
69+
},
70+
],
71+
},
72+
};
73+
74+
const output = CaseConverter.transformKeysFromSnakeToCamel(input);
75+
76+
expect(output).toEqual({
77+
nestedObject: {
78+
withArray: [
79+
{
80+
ofObjects: {
81+
withRandomString: randomString,
82+
},
83+
},
84+
],
85+
},
86+
});
87+
});
88+
});
89+
90+
describe('transformKeysFromCamelToSnake', () => {
91+
it.each([
92+
'string',
93+
42,
94+
BigInt(0),
95+
new Date(),
96+
Symbol('test'),
97+
true,
98+
null,
99+
undefined,
100+
])('should not transform primitive [%#]', (value: unknown) => {
101+
expect(CaseConverter.transformKeysFromCamelToSnake(value)).toEqual(value);
102+
});
103+
104+
it('should not transform simple array', () => {
105+
const input = faker.helpers.multiple(() => faker.string.sample());
106+
107+
const output = CaseConverter.transformKeysFromCamelToSnake(input);
108+
109+
expect(output).toEqual(input);
110+
});
111+
112+
it('should transform array of objects', () => {
113+
const input = faker.helpers.multiple(() => ({
114+
testCase: faker.string.sample(),
115+
}));
116+
const expectedOutput = input.map((v) => ({
117+
test_case: v.testCase,
118+
}));
119+
120+
const output = CaseConverter.transformKeysFromCamelToSnake(input);
121+
122+
expect(output).toEqual(expectedOutput);
123+
});
124+
125+
it('should transform plain object to camelCase', () => {
126+
const input = {
127+
randomString: faker.string.sample(),
128+
randomNumber: faker.number.float(),
129+
randomBoolean: faker.datatype.boolean(),
130+
alwaysNull: null,
131+
};
132+
133+
const output = CaseConverter.transformKeysFromCamelToSnake(input);
134+
135+
expect(output).toEqual({
136+
random_string: input.randomString,
137+
random_number: input.randomNumber,
138+
random_boolean: input.randomBoolean,
139+
always_null: null,
140+
});
141+
});
142+
143+
it('should transform input with nested data', () => {
144+
const randomString = faker.string.sample();
145+
146+
const input = {
147+
nestedObject: {
148+
withArray: [
149+
{
150+
ofObjects: {
151+
withRandomString: randomString,
152+
},
153+
},
154+
],
155+
},
156+
};
157+
158+
const output = CaseConverter.transformKeysFromCamelToSnake(input);
159+
160+
expect(output).toEqual({
161+
nested_object: {
162+
with_array: [
163+
{
164+
of_objects: {
165+
with_random_string: randomString,
166+
},
167+
},
168+
],
169+
},
170+
});
171+
});
172+
});
173+
});
Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,48 @@
1-
export class CaseConverter {
2-
static transformToCamelCase(input: any): any {
3-
if (typeof input === 'string') {
4-
return input.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
5-
} else if (Array.isArray(input)) {
6-
return input.map((item) => CaseConverter.transformToCamelCase(item));
7-
} else if (typeof input === 'object' && input !== null) {
8-
return Object.keys(input).reduce(
9-
(acc: Record<string, any>, key: string) => {
10-
const camelCaseKey = key.replace(/_([a-z])/g, (g) =>
11-
g[1].toUpperCase(),
12-
);
13-
acc[camelCaseKey] = CaseConverter.transformToCamelCase(input[key]);
14-
return acc;
15-
},
16-
{},
17-
);
18-
} else {
19-
return input;
20-
}
1+
type CaseTransformer = (input: string) => string;
2+
3+
/**
4+
* TODO: check if replacing it with lodash.camelCase
5+
* won't break anything
6+
*/
7+
export const snakeToCamel: CaseTransformer = (input) => {
8+
return input.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
9+
};
10+
11+
/**
12+
* TODO: check if replacing it with lodash.snakeCase
13+
* won't break anything
14+
*/
15+
export const camelToSnake: CaseTransformer = (input) => {
16+
return input.replace(/([A-Z])/g, '_$1').toLowerCase();
17+
};
18+
19+
function transformKeysCase(
20+
input: unknown,
21+
transformer: CaseTransformer,
22+
): unknown {
23+
/**
24+
* Primitives and Date objects returned as is
25+
* to keep their original value for later use
26+
*/
27+
if (input === null || typeof input !== 'object' || input instanceof Date) {
28+
return input;
29+
}
30+
31+
if (Array.isArray(input)) {
32+
return input.map((value) => transformKeysCase(value, transformer));
2133
}
2234

23-
static transformToSnakeCase(input: any): any {
24-
function camelToSnakeKey(id: string): string {
25-
if (!id) return id;
26-
const parts = id
27-
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
28-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
29-
.split(/[\s_-]+/)
30-
.filter(Boolean);
31-
return parts.map((p) => p.toLowerCase()).join('_');
32-
}
33-
if (typeof input === 'string') {
34-
return camelToSnakeKey(input);
35-
} else if (Array.isArray(input)) {
36-
return input.map((item) => CaseConverter.transformToSnakeCase(item));
37-
} else if (typeof input === 'object' && input !== null) {
38-
return Object.keys(input).reduce(
39-
(acc: Record<string, any>, key: string) => {
40-
const snakeCaseKey = camelToSnakeKey(key);
41-
acc[snakeCaseKey] = CaseConverter.transformToSnakeCase(input[key]);
42-
return acc;
43-
},
44-
{},
45-
);
46-
} else {
47-
return input;
48-
}
35+
const transformedObject: Record<string, unknown> = {};
36+
for (const [key, value] of Object.entries(input)) {
37+
transformedObject[transformer(key)] = transformKeysCase(value, transformer);
4938
}
39+
return transformedObject;
40+
}
41+
42+
export function transformKeysFromSnakeToCamel(input: unknown): unknown {
43+
return transformKeysCase(input, snakeToCamel);
44+
}
45+
46+
export function transformKeysFromCamelToSnake(input: unknown): unknown {
47+
return transformKeysCase(input, camelToSnake);
5048
}

packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { HEADER_SIGNATURE_KEY } from '../../common/constant';
1111
import { ErrorWebhook } from '../../common/constant/errors';
1212
import { EventType, WebhookStatus } from '../../common/enums/webhook';
1313
import { ValidationError } from '../../common/errors';
14-
import { CaseConverter } from '../../common/utils/case-converter';
14+
import { transformKeysFromCamelToSnake } from '../../common/utils/case-converter';
1515
import { formatAxiosError } from '../../common/utils/http';
1616
import { signMessage } from '../../common/utils/signature';
1717
import { JobService } from '../job/job.service';
@@ -107,7 +107,7 @@ export class WebhookService {
107107
reason: webhook.failureDetail,
108108
};
109109
}
110-
const transformedWebhook = CaseConverter.transformToSnakeCase(webhookData);
110+
const transformedWebhook: any = transformKeysFromCamelToSnake(webhookData);
111111

112112
const signedBody = await signMessage(
113113
transformedWebhook,

packages/apps/fortune/recording-oracle/src/common/interceptors/snake-case.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,26 @@ import {
66
} from '@nestjs/common';
77
import { Observable } from 'rxjs';
88
import { map } from 'rxjs/operators';
9-
import { CaseConverter } from '../utils/case-converter';
9+
import {
10+
transformKeysFromCamelToSnake,
11+
transformKeysFromSnakeToCamel,
12+
} from '../utils/case-converter';
1013

1114
@Injectable()
1215
export class SnakeCaseInterceptor implements NestInterceptor {
1316
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
1417
const request = context.switchToHttp().getRequest();
1518

1619
if (request.body) {
17-
request.body = CaseConverter.transformToCamelCase(request.body);
20+
request.body = transformKeysFromSnakeToCamel(request.body);
1821
}
1922

2023
if (request.query) {
21-
request.query = CaseConverter.transformToCamelCase(request.query);
24+
request.query = transformKeysFromSnakeToCamel(request.query);
2225
}
2326

2427
return next
2528
.handle()
26-
.pipe(map((data) => CaseConverter.transformToSnakeCase(data)));
29+
.pipe(map((data) => transformKeysFromCamelToSnake(data)));
2730
}
2831
}

packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
22
Injectable,
3-
ValidationError as ValidError,
43
ValidationPipe,
54
ValidationPipeOptions,
5+
ValidationError as ValidError,
66
} from '@nestjs/common';
77
import { ValidationError } from '../errors';
8-
import { CaseConverter } from '../utils/case-converter';
8+
import { camelToSnake } from '../utils/case-converter';
99

1010
@Injectable()
1111
export class HttpValidationPipe extends ValidationPipe {
@@ -34,9 +34,7 @@ export class HttpValidationPipe extends ValidationPipe {
3434

3535
const transformedPath = rawPath
3636
.split('.')
37-
.map((segment) =>
38-
segment.replace(/^[A-Za-z0-9_]+/, CaseConverter.transformToSnakeCase),
39-
)
37+
.map((segment) => segment.replace(/^[A-Za-z0-9_]+/, camelToSnake))
4038
.join('.');
4139

4240
return `${transformedPath} ${rest}`;

0 commit comments

Comments
 (0)