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(unique): move to helpers #1298

Merged
merged 6 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class Faker {
readonly definitions: LocaleDefinition = this.initDefinitions();

readonly fake: Fake['fake'] = new Fake(this).fake;
readonly unique: Unique['unique'] = new Unique().unique;
readonly unique: Unique['unique'] = new Unique(this).unique;

readonly mersenne: Mersenne = new Mersenne();
readonly random: Random = new Random(this);
Expand Down
44 changes: 44 additions & 0 deletions src/modules/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Faker } from '../..';
import { FakerError } from '../../errors/faker-error';
import type { RecordKey } from '../unique/unique';
import * as uniqueExec from '../unique/unique';
xDivisionByZerox marked this conversation as resolved.
Show resolved Hide resolved
import { luhnCheckValue } from './luhn-check';

/**
Expand Down Expand Up @@ -585,4 +587,46 @@ export class Helpers {
// return the response recursively until we are done finding all tags
return this.fake(res);
}

/**
* Generates a unique result using the results of the given method.
* Used unique entries will be stored internally and filtered from subsequent calls.
*
* @template Method The type of the method to execute.
* @param method The method used to generate the values.
* @param args The arguments used to call the method.
* @param options The optional options used to configure this method.
* @param options.startTime This parameter does nothing.
* @param options.maxTime The time in milliseconds this method may take before throwing an error. Defaults to `50`.
* @param options.maxRetries The total number of attempts to try before throwing an error. Defaults to `50`.
* @param options.currentIterations This parameter does nothing.
* @param options.exclude The value or values that should be excluded/skipped. Defaults to `[]`.
* @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key.
* @param options.store The store of unique entries. Defaults to a global store.
*
* @example
* faker.helpers.unique(faker.name.firstName) // 'Corbin'
*/
unique<Method extends (...parameters) => RecordKey>(
method: Method,
args?: Parameters<Method>,
options: {
startTime?: number;
maxTime?: number;
maxRetries?: number;
currentIterations?: number;
exclude?: RecordKey | RecordKey[];
compare?: (obj: Record<RecordKey, RecordKey>, key: RecordKey) => 0 | -1;
store?: Record<RecordKey, RecordKey>;
} = {}
): ReturnType<Method> {
const { maxTime = 50, maxRetries = 50 } = options;
return uniqueExec.exec(method, args, {
...options,
startTime: new Date().getTime(),
maxTime,
maxRetries,
currentIterations: 0,
});
}
}
24 changes: 15 additions & 9 deletions src/modules/unique/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { Faker } from '../..';
import { deprecated } from '../../internal/deprecated';
import type { RecordKey } from './unique';
import * as uniqueExec from './unique';

/**
* Module to generate unique entries.
*
* @deprecated
*/
export class Unique {
constructor() {
constructor(private readonly faker: Faker) {
xDivisionByZerox marked this conversation as resolved.
Show resolved Hide resolved
// Bind `this` so namespaced is working correctly
for (const name of Object.getOwnPropertyNames(Unique.prototype)) {
if (
Expand Down Expand Up @@ -36,8 +39,12 @@ export class Unique {
* @param options.compare The function used to determine whether a value was already returned. Defaults to check the existence of the key.
* @param options.store The store of unique entries. Defaults to a global store.
*
* @see faker.helpers.unique()
*
* @example
* faker.unique(faker.name.firstName) // 'Corbin'
*
* @deprecated Use faker.helpers.unique() instead.
*/
unique<Method extends (...parameters) => RecordKey>(
method: Method,
Expand All @@ -52,13 +59,12 @@ export class Unique {
store?: Record<RecordKey, RecordKey>;
} = {}
): ReturnType<Method> {
const { maxTime = 50, maxRetries = 50 } = options;
return uniqueExec.exec(method, args, {
...options,
startTime: new Date().getTime(),
maxTime,
maxRetries,
currentIterations: 0,
deprecated({
deprecated: 'faker.fake()',
proposed: 'faker.helpers.fake()',
xDivisionByZerox marked this conversation as resolved.
Show resolved Hide resolved
since: '7.5',
until: '8.0',
});
return this.faker.helpers.unique(method, args, options);
}
}
4 changes: 2 additions & 2 deletions src/modules/unique/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type RecordKey = string | number | symbol;

/**
* Global store of unique values.
* This means that faker should *never* return duplicate values across all API methods when using `Faker.unique` without passing `options.store`.
* This means that faker should *never* return duplicate values across all API methods when using `Faker.helpers.unique` without passing `options.store`.
*/
const GLOBAL_UNIQUE_STORE: Record<RecordKey, RecordKey> = {};

Expand Down Expand Up @@ -60,7 +60,7 @@ total time: ${now - startTime}ms`
`${code} for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.unique().`
Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`
);
}

Expand Down
24 changes: 24 additions & 0 deletions test/__snapshots__/helpers.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ exports[`helpers > 42 > slugify > noArgs 1`] = `""`;

exports[`helpers > 42 > slugify > some string 1`] = `"hello-world"`;

exports[`helpers > 42 > unique > with () => number 1`] = `37454`;

exports[`helpers > 42 > unique > with () => number and args 1`] = `19`;

exports[`helpers > 42 > unique > with customMethod 1`] = `"Test-188"`;

exports[`helpers > 42 > unique > with customMethod and args 1`] = `"prefix-1-Test-188"`;

exports[`helpers > 42 > uniqueArray > with array 1`] = `
[
"H",
Expand Down Expand Up @@ -212,6 +220,14 @@ exports[`helpers > 1211 > slugify > noArgs 1`] = `""`;

exports[`helpers > 1211 > slugify > some string 1`] = `"hello-world"`;

exports[`helpers > 1211 > unique > with () => number 1`] = `92852`;

exports[`helpers > 1211 > unique > with () => number and args 1`] = `47`;

exports[`helpers > 1211 > unique > with customMethod 1`] = `"Test-465"`;

exports[`helpers > 1211 > unique > with customMethod and args 1`] = `"prefix-1-Test-465"`;

exports[`helpers > 1211 > uniqueArray > with array 1`] = `
[
"W",
Expand Down Expand Up @@ -316,6 +332,14 @@ exports[`helpers > 1337 > slugify > noArgs 1`] = `""`;

exports[`helpers > 1337 > slugify > some string 1`] = `"hello-world"`;

exports[`helpers > 1337 > unique > with () => number 1`] = `26202`;

exports[`helpers > 1337 > unique > with () => number and args 1`] = `13`;

exports[`helpers > 1337 > unique > with customMethod 1`] = `"Test-132"`;

exports[`helpers > 1337 > unique > with customMethod and args 1`] = `"prefix-1-Test-132"`;

exports[`helpers > 1337 > uniqueArray > with array 1`] = `
[
"o",
Expand Down
159 changes: 159 additions & 0 deletions test/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { seededTests } from './support/seededRuns';

const NON_SEEDED_BASED_RUN = 5;

function customUniqueMethod(prefix: string = ''): string {
const element = faker.helpers.arrayElement(
Array.from({ length: 500 }, (_, index) => `Test-${index + 1}`)
);
return `${prefix}${element}`;
}

describe('helpers', () => {
afterEach(() => {
faker.locale = 'en';
Expand Down Expand Up @@ -93,6 +100,13 @@ describe('helpers', () => {
'my string: {{datatype.string}}'
);
});

t.describe('unique', (t) => {
t.it('with customMethod', customUniqueMethod)
.it('with customMethod and args', customUniqueMethod, ['prefix-1-'])
.it('with () => number', faker.datatype.number)
.it('with () => number and args', faker.datatype.number, [50]);
});
});

describe(`random seeded tests for seed ${faker.seed()}`, () => {
Expand Down Expand Up @@ -608,6 +622,151 @@ describe('helpers', () => {
delete (faker.random as any).special;
});
});

describe('unique()', () => {
it('should be possible to call a function with no arguments and return a result', () => {
const result = faker.helpers.unique(faker.internet.email);
expect(result).toBeTypeOf('string');
});

it('should be possible to call a function with arguments and return a result', () => {
const result = faker.helpers.unique(faker.internet.email, [
'fName',
'lName',
'domain',
]); // third argument is provider, or domain for email
expect(result).toMatch(/\@domain/);
});

it('should be possible to limit unique call by maxTime in ms', () => {
expect(() => {
faker.helpers.unique(faker.internet.protocol, [], {
maxTime: 1,
maxRetries: 9999,
exclude: ['https', 'http'],
});
}).toThrowError(
new FakerError(`Exceeded maxTime: 1 for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`)
);
});

it('should be possible to limit unique call by maxRetries', () => {
expect(() => {
faker.helpers.unique(faker.internet.protocol, [], {
maxTime: 5000,
maxRetries: 5,
exclude: ['https', 'http'],
});
}).toThrowError(
new FakerError(`Exceeded maxRetries: 5 for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`)
);
});

it('should throw a FakerError instance on error', () => {
expect(() => {
faker.helpers.unique(faker.internet.protocol, [], {
maxTime: 5000,
maxRetries: 5,
exclude: ['https', 'http'],
});
}).toThrowError(
new FakerError(`Exceeded maxRetries: 5 for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`)
);
});
});
}

// This test can be only executed once, because the unique function has a global state.
// See: https://github.com/faker-js/faker/issues/371
describe('global unique()', () => {
it('should be possible to exclude results as array', () => {
const internetProtocol = () =>
faker.helpers.arrayElement(['https', 'http']);
const result = faker.helpers.unique(internetProtocol, [], {
exclude: ['https'],
});
expect(result).toBe('http');
});

it('no conflict', () => {
let i = 0;
const method = () => `no conflict: ${i++}`;
expect(faker.helpers.unique(method)).toBe('no conflict: 0');
expect(faker.helpers.unique(method)).toBe('no conflict: 1');
});

it('with conflict', () => {
const method = () => 'with conflict: 0';
expect(faker.helpers.unique(method)).toBe('with conflict: 0');
expect(() =>
faker.helpers.unique(method, [], {
maxRetries: 1,
})
).toThrowError(
new FakerError(`Exceeded maxRetries: 1 for uniqueness check.

May not be able to generate any more unique values with current settings.
Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`)
);
});

it('should not mutate most of the input option properties', () => {
const method = () => 'options-mutate-test';

const startTime = new Date().getTime();
const maxTime = 49;
const maxRetries = 49;
const currentIterations = 0;
const exclude = [];
const compare = (obj, key) => (obj[key] === undefined ? -1 : 0);

const options = {
startTime,
maxTime,
maxRetries,
currentIterations,
exclude,
compare,
};

faker.helpers.unique(method, [], options);

expect(options.startTime).toBe(startTime);
expect(options.maxTime).toBe(maxTime);
expect(options.maxRetries).toBe(maxRetries);
// `options.currentIterations` is incremented in the `faker.helpers.unique` function.
expect(options.exclude).toBe(exclude);
expect(options.compare).toBe(compare);
});

it('should be possible to pass a user-specific store', () => {
const store = {};

const method = () => 'with conflict: 0';

expect(faker.helpers.unique(method, [], { store })).toBe(
'with conflict: 0'
);
expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' });

expect(() => faker.helpers.unique(method, [], { store })).toThrow();

delete store['with conflict: 0'];

expect(faker.helpers.unique(method, [], { store })).toBe(
'with conflict: 0'
);
expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' });
});
});
});
});
Loading