Skip to content

Commit

Permalink
refactor(unique): move to helpers (#1298)
Browse files Browse the repository at this point in the history
  • Loading branch information
xDivisionByZerox committed Aug 29, 2022
1 parent c2108fa commit 7f8b871
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 94 deletions.
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
Expand Up @@ -2,6 +2,8 @@ import type { Faker } from '../..';
import { FakerError } from '../../errors/faker-error';
import { deprecated } from '../../internal/deprecated';
import { luhnCheckValue } from './luhn-check';
import type { RecordKey } from './unique';
import * as uniqueExec from './unique';

/**
* Module with various helper methods that transform the method input rather than returning values from locales.
Expand Down Expand Up @@ -590,4 +592,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,
});
}
}
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
26 changes: 16 additions & 10 deletions src/modules/unique/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { RecordKey } from './unique';
import * as uniqueExec from './unique';
import type { Faker } from '../..';
import { deprecated } from '../../internal/deprecated';
import type { RecordKey } from '../helpers/unique';

/**
* Module to generate unique entries.
*
* @deprecated
*/
export class Unique {
constructor() {
constructor(private readonly faker: Faker) {
// 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.unique()',
proposed: 'faker.helpers.unique()',
since: '7.5',
until: '8.0',
});
return this.faker.helpers.unique(method, args, options);
}
}
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' });
});
});
});
});

0 comments on commit 7f8b871

Please sign in to comment.