Skip to content

Commit 43f5d71

Browse files
committed
refactor: transform helpers
1 parent 9108903 commit 43f5d71

4 files changed

Lines changed: 304 additions & 46 deletions

File tree

src/modules/helpers/_eval.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FakerError } from '../../errors/faker-error';
2-
import type { Faker } from '../../faker';
2+
import type { FakerCore } from '../../faker-core';
3+
import { arrayElement } from './array-element';
34

45
const REGEX_DOT_OR_BRACKET = /\.|\(/;
56

@@ -12,61 +13,63 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/;
1213
* It tries to resolve the expression on the given/default entrypoints:
1314
*
1415
* ```js
15-
* const firstName = fakeEval('person.firstName', faker);
16-
* const firstName2 = fakeEval('person.first_name', faker);
16+
* const firstName = fakeEval(fakerCore, 'person.firstName', [moduleRegistry]);
17+
* const firstName2 = fakeEval(fakerCore, 'person.first_name');
1718
* ```
1819
*
1920
* Is equivalent to:
2021
*
2122
* ```js
22-
* const firstName = faker.person.firstName();
23-
* const firstName2 = faker.helpers.arrayElement(faker.rawDefinitions.person.first_name);
23+
* const firstName = firstName(fakerCore);
24+
* const firstName2 = arrayElement(fakerCore, fakerCore.definitions.person.first_name);
2425
* ```
2526
*
2627
* You can provide parameters as well. At first, they will be parsed as json,
2728
* and if that isn't possible, it will fall back to string:
2829
*
2930
* ```js
30-
* const message = fakeEval('phone.number(+!# !## #### #####!)', faker);
31+
* const message = fakeEval(fakerCore, 'phone.number(+!# !## #### #####!)', [moduleRegistry]);
3132
* ```
3233
*
3334
* It is also possible to use multiple parameters (comma separated).
3435
*
3536
* ```js
36-
* const pin = fakeEval('string.numeric(4, {"allowLeadingZeros": true})', faker);
37+
* const pin = fakeEval(fakerCore, 'string.numeric(4, {"allowLeadingZeros": true})');
3738
* ```
3839
*
3940
* This method can resolve expressions with varying depths (dot separated parts).
4041
*
4142
* ```ts
42-
* const airlineModule = fakeEval('airline', faker); // AirlineModule
43-
* const airlineObject = fakeEval('airline.airline', faker); // { name: 'Etihad Airways', iataCode: 'EY' }
44-
* const airlineCode = fakeEval('airline.airline.iataCode', faker); // 'EY'
45-
* const airlineName = fakeEval('airline.airline().name', faker); // 'Etihad Airways'
46-
* const airlineMethodName = fakeEval('airline.airline.name', faker); // 'bound airline'
43+
* const airlineModule = fakeEval(fakerCore, 'airline'); // AirlineModule
44+
* const airlineObject = fakeEval(fakerCore, 'airline.airline'); // { name: 'Etihad Airways', iataCode: 'EY' }
45+
* const airlineCode = fakeEval(fakerCore, 'airline.airline.iataCode'); // 'EY'
46+
* const airlineName = fakeEval(fakerCore, 'airline.airline().name'); // 'Etihad Airways'
47+
* const airlineMethodName = fakeEval(fakerCore, 'airline.airline.name'); // 'bound airline'
4748
* ```
4849
*
4950
* It is NOT possible to access any values not passed as entrypoints.
5051
*
5152
* This method will never return arrays, as it will pick a random element from them instead.
5253
*
54+
* @param fakerCore The FakerCore to use.
5355
* @param expression The expression to evaluate on the entrypoints.
54-
* @param faker The faker instance to resolve array elements.
5556
* @param entrypoints The entrypoints to use when evaluating the expression.
57+
* Defaults to the locale definitions of the given fakerCore, but can be set to any array of objects/functions.
5658
*
57-
* @see faker.helpers.fake() If you wish to have a string with multiple expressions.
59+
* @see fake() If you wish to have a string with multiple expressions.
5860
*
5961
* @example
60-
* fakeEval('person.lastName', faker) // 'Barrows'
61-
* fakeEval('helpers.arrayElement(["heads", "tails"])', faker) // 'tails'
62-
* fakeEval('number.int(9999)', faker) // 4834
62+
* fakeEval(fakerCore, 'location.city_name'); // 'Panda City'
63+
* fakeEval(fakerCore, 'person.lastName', [moduleRegistry]); // 'Barrows'
64+
* fakeEval(fakerCore, 'helpers.arrayElement(["heads", "tails"])', [ { helpers: helpersModule }]); // 'tails'
65+
* fakeEval(fakerCore, 'number.int(9999)', [{ number: numberModule }]); // 4834
6366
*
6467
* @since 8.4.0
6568
*/
6669
export function fakeEval(
70+
fakerCore: FakerCore,
6771
expression: string,
68-
faker: Faker,
69-
entrypoints: ReadonlyArray<unknown> = [faker, faker.rawDefinitions]
72+
entrypoints: ReadonlyArray<unknown> = [fakerCore.definitions]
7073
): unknown {
7174
if (expression.length === 0) {
7275
throw new FakerError('Eval expression cannot be empty.');
@@ -81,7 +84,7 @@ export function fakeEval(
8184
do {
8285
let index: number;
8386
if (remaining.startsWith('(')) {
84-
[index, current] = evalProcessFunction(remaining, current);
87+
[index, current] = evalProcessFunction(fakerCore, remaining, current);
8588
} else {
8689
[index, current] = evalProcessExpression(remaining, current);
8790
}
@@ -92,7 +95,7 @@ export function fakeEval(
9295
current = current
9396
.filter((value) => value != null)
9497
.map((value): unknown =>
95-
Array.isArray(value) ? faker.helpers.arrayElement(value) : value
98+
Array.isArray(value) ? arrayElement(fakerCore, value) : value
9699
);
97100
} while (remaining.length > 0 && current.length > 0);
98101

@@ -101,16 +104,18 @@ export function fakeEval(
101104
}
102105

103106
const value = current[0];
104-
return typeof value === 'function' ? value() : value;
107+
return typeof value === 'function' ? value(fakerCore) : value;
105108
}
106109

107110
/**
108111
* Evaluates a function call and returns the new read index and the mapped results.
109112
*
113+
* @param fakerCore The FakerCore to use.
110114
* @param input The input string to parse.
111115
* @param entrypoints The entrypoints to attempt the call on.
112116
*/
113117
function evalProcessFunction(
118+
fakerCore: FakerCore,
114119
input: string,
115120
entrypoints: ReadonlyArray<unknown>
116121
): [continueIndex: number, mapped: unknown[]] {
@@ -133,7 +138,9 @@ function evalProcessFunction(
133138
return [
134139
index + (nextChar === '.' ? 2 : 1), // one for the closing bracket, one for the dot
135140
entrypoints.map((entrypoint): unknown =>
136-
typeof entrypoint === 'function' ? entrypoint(...params) : undefined
141+
typeof entrypoint === 'function'
142+
? entrypoint(fakerCore, ...params)
143+
: undefined
137144
),
138145
];
139146
}

src/modules/helpers/_temp-eval.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { FakerError } from '../../errors/faker-error';
2+
import type { Faker } from '../../faker';
3+
4+
const REGEX_DOT_OR_BRACKET = /\.|\(/;
5+
6+
/**
7+
* Resolves the given expression and returns its result. This method should only be used when using serialized expressions.
8+
*
9+
* This method is useful if you have to build a random string from a static, non-executable source
10+
* (e.g. string coming from a developer, stored in a database or a file).
11+
*
12+
* It tries to resolve the expression on the given/default entrypoints:
13+
*
14+
* ```js
15+
* const firstName = fakeEval('person.firstName', faker);
16+
* const firstName2 = fakeEval('person.first_name', faker);
17+
* ```
18+
*
19+
* Is equivalent to:
20+
*
21+
* ```js
22+
* const firstName = faker.person.firstName();
23+
* const firstName2 = faker.helpers.arrayElement(faker.rawDefinitions.person.first_name);
24+
* ```
25+
*
26+
* You can provide parameters as well. At first, they will be parsed as json,
27+
* and if that isn't possible, it will fall back to string:
28+
*
29+
* ```js
30+
* const message = fakeEval('phone.number(+!# !## #### #####!)', faker);
31+
* ```
32+
*
33+
* It is also possible to use multiple parameters (comma separated).
34+
*
35+
* ```js
36+
* const pin = fakeEval('string.numeric(4, {"allowLeadingZeros": true})', faker);
37+
* ```
38+
*
39+
* This method can resolve expressions with varying depths (dot separated parts).
40+
*
41+
* ```ts
42+
* const airlineModule = fakeEval('airline', faker); // AirlineModule
43+
* const airlineObject = fakeEval('airline.airline', faker); // { name: 'Etihad Airways', iataCode: 'EY' }
44+
* const airlineCode = fakeEval('airline.airline.iataCode', faker); // 'EY'
45+
* const airlineName = fakeEval('airline.airline().name', faker); // 'Etihad Airways'
46+
* const airlineMethodName = fakeEval('airline.airline.name', faker); // 'bound airline'
47+
* ```
48+
*
49+
* It is NOT possible to access any values not passed as entrypoints.
50+
*
51+
* This method will never return arrays, as it will pick a random element from them instead.
52+
*
53+
* @param expression The expression to evaluate on the entrypoints.
54+
* @param faker The faker instance to resolve array elements.
55+
* @param entrypoints The entrypoints to use when evaluating the expression.
56+
*
57+
* @see faker.helpers.fake() If you wish to have a string with multiple expressions.
58+
*
59+
* @example
60+
* fakeEval('person.lastName', faker) // 'Barrows'
61+
* fakeEval('helpers.arrayElement(["heads", "tails"])', faker) // 'tails'
62+
* fakeEval('number.int(9999)', faker) // 4834
63+
*
64+
* @since 8.4.0
65+
*/
66+
export function fakeEval(
67+
expression: string,
68+
faker: Faker,
69+
entrypoints: ReadonlyArray<unknown> = [faker, faker.rawDefinitions]
70+
): unknown {
71+
if (expression.length === 0) {
72+
throw new FakerError('Eval expression cannot be empty.');
73+
}
74+
75+
if (entrypoints.length === 0) {
76+
throw new FakerError('Eval entrypoints cannot be empty.');
77+
}
78+
79+
let current = entrypoints;
80+
let remaining = expression;
81+
do {
82+
let index: number;
83+
if (remaining.startsWith('(')) {
84+
[index, current] = evalProcessFunction(remaining, current);
85+
} else {
86+
[index, current] = evalProcessExpression(remaining, current);
87+
}
88+
89+
remaining = remaining.substring(index);
90+
91+
// Remove garbage and resolve array values
92+
current = current
93+
.filter((value) => value != null)
94+
.map((value): unknown =>
95+
Array.isArray(value) ? faker.helpers.arrayElement(value) : value
96+
);
97+
} while (remaining.length > 0 && current.length > 0);
98+
99+
if (current.length === 0) {
100+
throw new FakerError(`Cannot resolve expression '${expression}'`);
101+
}
102+
103+
const value = current[0];
104+
return typeof value === 'function' ? value() : value;
105+
}
106+
107+
/**
108+
* Evaluates a function call and returns the new read index and the mapped results.
109+
*
110+
* @param input The input string to parse.
111+
* @param entrypoints The entrypoints to attempt the call on.
112+
*/
113+
function evalProcessFunction(
114+
input: string,
115+
entrypoints: ReadonlyArray<unknown>
116+
): [continueIndex: number, mapped: unknown[]] {
117+
const [index, params] = findParams(input);
118+
const nextChar = input[index + 1];
119+
switch (nextChar) {
120+
case '.':
121+
case '(':
122+
case undefined: {
123+
break; // valid
124+
}
125+
126+
default: {
127+
throw new FakerError(
128+
`Expected dot ('.'), open parenthesis ('('), or nothing after function call but got '${nextChar}'`
129+
);
130+
}
131+
}
132+
133+
return [
134+
index + (nextChar === '.' ? 2 : 1), // one for the closing bracket, one for the dot
135+
entrypoints.map((entrypoint): unknown =>
136+
typeof entrypoint === 'function' ? entrypoint(...params) : undefined
137+
),
138+
];
139+
}
140+
141+
/**
142+
* Tries to find the parameters of a function call.
143+
*
144+
* @param input The input string to parse.
145+
*/
146+
function findParams(input: string): [continueIndex: number, params: unknown[]] {
147+
let index = input.indexOf(')', 1);
148+
if (index === -1) {
149+
throw new FakerError(`Missing closing parenthesis in '${input}'`);
150+
}
151+
152+
while (index !== -1) {
153+
const params = input.substring(1, index);
154+
try {
155+
// assuming that the params are valid JSON
156+
return [index, JSON.parse(`[${params}]`) as unknown[]];
157+
} catch {
158+
if (!params.includes("'") && !params.includes('"')) {
159+
try {
160+
// assuming that the params are a single unquoted string
161+
return [index, JSON.parse(`["${params}"]`) as unknown[]];
162+
} catch {
163+
// try again with the next index
164+
}
165+
}
166+
}
167+
168+
index = input.indexOf(')', index + 1);
169+
}
170+
171+
index = input.lastIndexOf(')');
172+
const params = input.substring(1, index);
173+
return [index, [params]];
174+
}
175+
176+
/**
177+
* Processes one expression part and returns the new read index and the mapped results.
178+
*
179+
* @param input The input string to parse.
180+
* @param entrypoints The entrypoints to resolve on.
181+
*/
182+
function evalProcessExpression(
183+
input: string,
184+
entrypoints: ReadonlyArray<unknown>
185+
): [continueIndex: number, mapped: unknown[]] {
186+
const result = REGEX_DOT_OR_BRACKET.exec(input);
187+
const dotMatch = (result?.[0] ?? '') === '.';
188+
const index = result?.index ?? input.length;
189+
const key = input.substring(0, index);
190+
if (key.length === 0) {
191+
throw new FakerError(`Expression parts cannot be empty in '${input}'`);
192+
}
193+
194+
const next = input[index + 1];
195+
if (dotMatch && (next == null || next === '.' || next === '(')) {
196+
throw new FakerError(`Found dot without property name in '${input}'`);
197+
}
198+
199+
return [
200+
index + (dotMatch ? 1 : 0),
201+
entrypoints.map((entrypoint) => resolveProperty(entrypoint, key)),
202+
];
203+
}
204+
205+
/**
206+
* Resolves the given property on the given entrypoint.
207+
*
208+
* @param entrypoint The entrypoint to resolve the property on.
209+
* @param key The property name to resolve.
210+
*/
211+
function resolveProperty(entrypoint: unknown, key: string): unknown {
212+
switch (typeof entrypoint) {
213+
case 'function': {
214+
try {
215+
entrypoint = entrypoint();
216+
} catch {
217+
return undefined;
218+
}
219+
220+
return entrypoint?.[key as keyof typeof entrypoint];
221+
}
222+
223+
case 'object': {
224+
return entrypoint?.[key as keyof typeof entrypoint];
225+
}
226+
227+
default: {
228+
return undefined;
229+
}
230+
}
231+
}

0 commit comments

Comments
 (0)