Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(fake): move to helpers #1161

Merged
merged 12 commits into from
Jul 30, 2022
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ The API covers the following modules:

### Templates

Faker contains a generator method `faker.fake` for combining faker API methods using a mustache string format.
Faker contains a generator method `faker.helpers.fake` for combining faker API methods using a mustache string format.

```ts
console.log(
faker.fake('Hello {{name.prefix}} {{name.lastName}}, how are you today?')
faker.helpers.fake(
'Hello {{name.prefix}} {{name.lastName}}, how are you today?'
)
);
```

Expand Down
6 changes: 3 additions & 3 deletions src/modules/address/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class Address {
format = this.faker.datatype.number(formats.length - 1);
}

return this.faker.fake(formats[format]);
return this.faker.helpers.fake(formats[format]);
}

/**
Expand Down Expand Up @@ -171,7 +171,7 @@ export class Address {
const format = this.faker.helpers.arrayElement(
this.faker.definitions.address.street
);
return this.faker.fake(format);
return this.faker.helpers.fake(format);
}

/**
Expand Down Expand Up @@ -212,7 +212,7 @@ export class Address {
const formats = this.faker.definitions.address.street_address;
const format = formats[useFullAddress ? 'full' : 'normal'];

return this.faker.fake(format);
return this.faker.helpers.fake(format);
}

/**
Expand Down
14 changes: 7 additions & 7 deletions src/modules/company/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class Company {
format = this.faker.datatype.number(formats.length - 1);
}

return this.faker.fake(formats[format]);
return this.faker.helpers.fake(formats[format]);
}

/**
Expand Down Expand Up @@ -88,9 +88,11 @@ export class Company {
* faker.company.catchPhrase() // 'Upgradable systematic flexibility'
*/
catchPhrase(): string {
return this.faker.fake(
'{{company.catchPhraseAdjective}} {{company.catchPhraseDescriptor}} {{company.catchPhraseNoun}}'
);
return [
this.catchPhraseAdjective(),
this.catchPhraseDescriptor(),
this.catchPhraseNoun(),
].join(' ');
}

/**
Expand All @@ -100,9 +102,7 @@ export class Company {
* faker.company.bs() // 'cultivate synergistic e-markets'
*/
bs(): string {
return this.faker.fake(
'{{company.bsBuzz}} {{company.bsAdjective}} {{company.bsNoun}}'
);
return [this.bsBuzz(), this.bsAdjective(), this.bsNoun()].join(' ');
}

/**
Expand Down
100 changes: 13 additions & 87 deletions src/modules/fake/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Faker } from '../..';
import { FakerError } from '../../errors/faker-error';
import { deprecated } from '../../internal/deprecated';

/**
* Generator method for combining faker methods based on string input.
*
* @deprecated
*/
export class Fake {
constructor(private readonly faker: Faker) {
Expand Down Expand Up @@ -47,6 +49,7 @@ export class Fake {
* @param str The template string that will get interpolated. Must not be empty.
*
* @see faker.helpers.mustache() to use custom functions for resolution.
* @see faker.helpers.fake()
*
* @example
* faker.fake('{{name.lastName}}') // 'Barrows'
Expand All @@ -55,93 +58,16 @@ export class Fake {
* faker.fake('Good Morning {{name.firstName}}!') // 'Good Morning Estelle!'
* faker.fake('You can call me at {{phone.number(!## ### #####!)}}.') // 'You can call me at 202 555 973722.'
* faker.fake('I flipped the coin an got: {{helpers.arrayElement(["heads", "tails"])}}') // 'I flipped the coin an got: tails'
*
* @deprecated Use faker.helpers.fake() instead.
*/
fake(str: string): string {
// if incoming str parameter is not provided, return error message
if (typeof str !== 'string' || str.length === 0) {
throw new FakerError('string parameter is required!');
}

// find first matching {{ and }}
const start = str.search(/{{[a-z]/);
const end = str.indexOf('}}', start);

// if no {{ and }} is found, we are done
if (start === -1 || end === -1) {
return str;
}

// extract method name from between the {{ }} that we found
// for example: {{name.firstName}}
const token = str.substring(start + 2, end + 2);
let method = token.replace('}}', '').replace('{{', '');

// extract method parameters
const regExp = /\(([^)]+)\)/;
const matches = regExp.exec(method);
let parameters = '';
if (matches) {
method = method.replace(regExp, '');
parameters = matches[1];
}

// split the method into module and function
const parts = method.split('.');

let currentModuleOrMethod: unknown = this.faker;
let currentDefinitions: unknown = this.faker.definitions;

// Search for the requested method or definition
for (const part of parts) {
currentModuleOrMethod = currentModuleOrMethod?.[part];
currentDefinitions = currentDefinitions?.[part];
}

// Make method executable
let fn: (args?: unknown) => unknown;
if (typeof currentModuleOrMethod === 'function') {
fn = currentModuleOrMethod as (args?: unknown) => unknown;
} else if (Array.isArray(currentDefinitions)) {
fn = () =>
this.faker.helpers.arrayElement(currentDefinitions as unknown[]);
} else {
throw new FakerError(`Invalid module method or definition: ${method}
- faker.${method} is not a function
- faker.definitions.${method} is not an array`);
}

// assign the function from the module.function namespace
fn = fn.bind(this);

// If parameters are populated here, they are always going to be of string type
// since we might actually be dealing with an object or array,
// we always attempt to the parse the incoming parameters into JSON
let params: unknown;
// Note: we experience a small performance hit here due to JSON.parse try / catch
// If anyone actually needs to optimize this specific code path, please open a support issue on github
try {
params = JSON.parse(parameters);
} catch (err) {
// since JSON.parse threw an error, assume parameters was actually a string
params = parameters;
}

let result: string;
if (typeof params === 'string' && params.length === 0) {
result = String(fn());
} else {
result = String(fn(params));
}

// Replace the found tag with the returned fake value
// We cannot use string.replace here because the result might contain evaluated characters
const res = str.substring(0, start) + result + str.substring(end + 2);

if (res === '') {
return '';
}

// return the response recursively until we are done finding all tags
return this.fake(res);
deprecated({
deprecated: 'faker.fake()',
proposed: 'faker.helpers.fake()',
since: '7.4',
until: '8.0',
});
return this.faker.helpers.fake(str);
}
}
131 changes: 131 additions & 0 deletions src/modules/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Faker } from '../..';
import { FakerError } from '../../errors/faker-error';
import { luhnCheckValue } from './luhn-check';

/**
Expand Down Expand Up @@ -454,4 +455,134 @@ export class Helpers {

return arrayCopy.slice(min);
}

/**
* Generator for combining faker methods based on a static string input.
*
* Note: We recommend using string template literals instead of `fake()`,
* which are faster and strongly typed (if you are using TypeScript),
* e.g. ``const address = `${faker.address.zipCode()} ${faker.address.city()}`;``
*
* This method is useful if you have to build a random string from a static, non-executable source
* (e.g. string coming from a user, stored in a database or a file).
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
*
* It checks the given string for placeholders and replaces them by calling faker methods:
*
* ```js
* const hello = faker.helpers.fake('Hi, my name is {{name.firstName}} {{name.lastName}}!')
* ```
*
* This would use the `faker.name.firstName()` and `faker.name.lastName()` method to resolve the placeholders respectively.
*
* It is also possible to provide parameters. At first, they will be parsed as json,
* and if that isn't possible, we will fall back to string:
*
* ```js
* const message = faker.helpers.fake(`You can call me at {{phone.number(+!# !## #### #####!)}}.')
* ```
*
* Currently it is not possible to set more than a single parameter.
*
* It is also NOT possible to use any non-faker methods or plain javascript in such templates.
*
* @param str The template string that will get interpolated. Must not be empty.
*
* @see faker.helpers.mustache() to use custom functions for resolution.
*
* @example
* faker.helpers.fake('{{name.lastName}}') // 'Barrows'
* faker.helpers.fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}') // 'Durgan, Noe MD'
* faker.helpers.fake('This is static test.') // 'This is static test.'
* faker.helpers.fake('Good Morning {{name.firstName}}!') // 'Good Morning Estelle!'
* faker.helpers.fake('You can call me at {{phone.number(!## ### #####!)}}.') // 'You can call me at 202 555 973722.'
* faker.helpers.fake('I flipped the coin an got: {{helpers.arrayElement(["heads", "tails"])}}') // 'I flipped the coin an got: tails'
*/
fake(str: string): string {
// if incoming str parameter is not provided, return error message
if (typeof str !== 'string' || str.length === 0) {
throw new FakerError('string parameter is required!');
}

// find first matching {{ and }}
const start = str.search(/{{[a-z]/);
const end = str.indexOf('}}', start);

// if no {{ and }} is found, we are done
if (start === -1 || end === -1) {
return str;
}

// extract method name from between the {{ }} that we found
// for example: {{name.firstName}}
const token = str.substring(start + 2, end + 2);
let method = token.replace('}}', '').replace('{{', '');

// extract method parameters
const regExp = /\(([^)]+)\)/;
const matches = regExp.exec(method);
let parameters = '';
if (matches) {
method = method.replace(regExp, '');
parameters = matches[1];
}

// split the method into module and function
const parts = method.split('.');

let currentModuleOrMethod: unknown = this.faker;
let currentDefinitions: unknown = this.faker.definitions;

// Search for the requested method or definition
for (const part of parts) {
currentModuleOrMethod = currentModuleOrMethod?.[part];
currentDefinitions = currentDefinitions?.[part];
}

// Make method executable
let fn: (args?: unknown) => unknown;
if (typeof currentModuleOrMethod === 'function') {
fn = currentModuleOrMethod as (args?: unknown) => unknown;
} else if (Array.isArray(currentDefinitions)) {
fn = () =>
this.faker.helpers.arrayElement(currentDefinitions as unknown[]);
} else {
throw new FakerError(`Invalid module method or definition: ${method}
- faker.${method} is not a function
- faker.definitions.${method} is not an array`);
}

// assign the function from the module.function namespace
fn = fn.bind(this);

// If parameters are populated here, they are always going to be of string type
// since we might actually be dealing with an object or array,
// we always attempt to the parse the incoming parameters into JSON
let params: unknown;
// Note: we experience a small performance hit here due to JSON.parse try / catch
// If anyone actually needs to optimize this specific code path, please open a support issue on github
try {
params = JSON.parse(parameters);
} catch (err) {
// since JSON.parse threw an error, assume parameters was actually a string
params = parameters;
}

let result: string;
if (typeof params === 'string' && params.length === 0) {
result = String(fn());
} else {
result = String(fn(params));
}

// Replace the found tag with the returned fake value
// We cannot use string.replace here because the result might contain evaluated characters
const res = str.substring(0, start) + result + str.substring(end + 2);

if (res === '') {
return '';
}

// return the response recursively until we are done finding all tags
return this.fake(res);
}
}
12 changes: 12 additions & 0 deletions test/__snapshots__/helpers.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ exports[`helpers > 42 > arrayElements > with array 2`] = `
]
`;

exports[`helpers > 42 > fake > with args 1`] = `"my string: Cky2eiXX/J"`;

exports[`helpers > 42 > fake > with plain string 1`] = `"my test string"`;

exports[`helpers > 42 > maybe > with only value 1`] = `"Hello World!"`;

exports[`helpers > 42 > maybe > with value and probability 1`] = `undefined`;
Expand Down Expand Up @@ -139,6 +143,10 @@ exports[`helpers > 1211 > arrayElements > with array 2`] = `
]
`;

exports[`helpers > 1211 > fake > with args 1`] = `"my string: wKti5-}$_/"`;

exports[`helpers > 1211 > fake > with plain string 1`] = `"my test string"`;

exports[`helpers > 1211 > maybe > with only value 1`] = `undefined`;

exports[`helpers > 1211 > maybe > with value and probability 1`] = `undefined`;
Expand Down Expand Up @@ -239,6 +247,10 @@ exports[`helpers > 1337 > arrayElements > with array 2`] = `
]
`;

exports[`helpers > 1337 > fake > with args 1`] = `"my string: 9U/4:SK$>6"`;

exports[`helpers > 1337 > fake > with plain string 1`] = `"my test string"`;

exports[`helpers > 1337 > maybe > with only value 1`] = `"Hello World!"`;

exports[`helpers > 1337 > maybe > with value and probability 1`] = `undefined`;
Expand Down
4 changes: 2 additions & 2 deletions test/all_functional.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('functional tests', () => {
}
});

describe('faker.fake functional tests', () => {
describe('faker.helpers.fake functional tests', () => {
for (const locale in faker.locales) {
describe(locale, () => {
Object.keys(modules).forEach((module) => {
Expand All @@ -109,7 +109,7 @@ describe('faker.fake functional tests', () => {
faker.locale = locale;
// TODO ST-DDT 2022-03-28: Use random seed once there are no more failures
faker.seed(1);
const result = faker.fake(`{{${module}.${meth}}}`);
const result = faker.helpers.fake(`{{${module}.${meth}}}`);

expect(result).toBeTypeOf('string');
});
Expand Down