From 0e96784f6c70be18f563235e28a0b36e54bdfb57 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Wed, 24 Aug 2022 21:40:47 +0200 Subject: [PATCH 1/4] feat(helpers): unique --- src/modules/helpers/index.ts | 44 +++++++ src/modules/unique/unique.ts | 4 +- test/__snapshots__/helpers.spec.ts.snap | 24 ++++ test/helpers.spec.ts | 159 ++++++++++++++++++++++++ test/unique.spec.ts | 84 +------------ 5 files changed, 232 insertions(+), 83 deletions(-) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index 6f94fe63099..c8b04a5bbc4 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -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'; import { luhnCheckValue } from './luhn-check'; /** @@ -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 RecordKey>( + method: Method, + args?: Parameters, + options: { + startTime?: number; + maxTime?: number; + maxRetries?: number; + currentIterations?: number; + exclude?: RecordKey | RecordKey[]; + compare?: (obj: Record, key: RecordKey) => 0 | -1; + store?: Record; + } = {} + ): ReturnType { + const { maxTime = 50, maxRetries = 50 } = options; + return uniqueExec.exec(method, args, { + ...options, + startTime: new Date().getTime(), + maxTime, + maxRetries, + currentIterations: 0, + }); + } } diff --git a/src/modules/unique/unique.ts b/src/modules/unique/unique.ts index 70408faf1c5..31fddec8c94 100644 --- a/src/modules/unique/unique.ts +++ b/src/modules/unique/unique.ts @@ -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 = {}; @@ -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().` ); } diff --git a/test/__snapshots__/helpers.spec.ts.snap b/test/__snapshots__/helpers.spec.ts.snap index c79f19c886c..0fa011fa041 100644 --- a/test/__snapshots__/helpers.spec.ts.snap +++ b/test/__snapshots__/helpers.spec.ts.snap @@ -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", @@ -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", @@ -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", diff --git a/test/helpers.spec.ts b/test/helpers.spec.ts index 61d742a7118..ce8fb73387e 100644 --- a/test/helpers.spec.ts +++ b/test/helpers.spec.ts @@ -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'; @@ -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()}`, () => { @@ -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' }); + }); + }); }); }); diff --git a/test/unique.spec.ts b/test/unique.spec.ts index 9644cd67d4e..e603939d2de 100644 --- a/test/unique.spec.ts +++ b/test/unique.spec.ts @@ -86,7 +86,7 @@ describe('unique', () => { 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.unique().`) +Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`) ); }); @@ -101,7 +101,7 @@ Try adjusting maxTime or maxRetries parameters for faker.unique().`) 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.unique().`) +Try adjusting maxTime or maxRetries parameters for faker.helpers.unique().`) ); }); @@ -116,88 +116,10 @@ Try adjusting maxTime or maxRetries parameters for faker.unique().`) 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.unique().`) +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 - it('should be possible to exclude results as array', () => { - const internetProtocol = () => - faker.helpers.arrayElement(['https', 'http']); - const result = faker.unique(internetProtocol, [], { - exclude: ['https'], - }); - expect(result).toBe('http'); - }); - - it('no conflict', () => { - let i = 0; - const method = () => `no conflict: ${i++}`; - expect(faker.unique(method)).toBe('no conflict: 0'); - expect(faker.unique(method)).toBe('no conflict: 1'); - }); - - it('with conflict', () => { - const method = () => 'with conflict: 0'; - expect(faker.unique(method)).toBe('with conflict: 0'); - expect(() => - faker.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.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.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.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.unique(method, [], { store })).toBe('with conflict: 0'); - expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' }); - - expect(() => faker.unique(method, [], { store })).toThrow(); - - delete store['with conflict: 0']; - - expect(faker.unique(method, [], { store })).toBe('with conflict: 0'); - expect(store).toEqual({ 'with conflict: 0': 'with conflict: 0' }); - }); }); From 228e5200daa0d7909d8a55c1298859421261da66 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Wed, 24 Aug 2022 21:51:14 +0200 Subject: [PATCH 2/4] refactor(unique): deprecate module --- src/faker.ts | 2 +- src/modules/unique/index.ts | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/faker.ts b/src/faker.ts index c84d77a4daf..9534e373a8c 100644 --- a/src/faker.ts +++ b/src/faker.ts @@ -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); diff --git a/src/modules/unique/index.ts b/src/modules/unique/index.ts index d77004e496c..aeef39324ed 100644 --- a/src/modules/unique/index.ts +++ b/src/modules/unique/index.ts @@ -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) { // Bind `this` so namespaced is working correctly for (const name of Object.getOwnPropertyNames(Unique.prototype)) { if ( @@ -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 RecordKey>( method: Method, @@ -52,13 +59,12 @@ export class Unique { store?: Record; } = {} ): ReturnType { - 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()', + since: '7.5', + until: '8.0', }); + return this.faker.helpers.unique(method, args, options); } } From 4c46a1487a99fe9b177c3daac7f9a1aa7e3f3a74 Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Sat, 27 Aug 2022 22:23:59 +0200 Subject: [PATCH 3/4] refactor: move unique implementation into helpers dir --- src/modules/helpers/index.ts | 4 ++-- src/modules/{unique => helpers}/unique.ts | 0 src/modules/unique/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/modules/{unique => helpers}/unique.ts (100%) diff --git a/src/modules/helpers/index.ts b/src/modules/helpers/index.ts index c8b04a5bbc4..bee765044f1 100644 --- a/src/modules/helpers/index.ts +++ b/src/modules/helpers/index.ts @@ -1,8 +1,8 @@ import type { Faker } from '../..'; import { FakerError } from '../../errors/faker-error'; -import type { RecordKey } from '../unique/unique'; -import * as uniqueExec from '../unique/unique'; 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. diff --git a/src/modules/unique/unique.ts b/src/modules/helpers/unique.ts similarity index 100% rename from src/modules/unique/unique.ts rename to src/modules/helpers/unique.ts diff --git a/src/modules/unique/index.ts b/src/modules/unique/index.ts index aeef39324ed..f8fe1787e65 100644 --- a/src/modules/unique/index.ts +++ b/src/modules/unique/index.ts @@ -1,6 +1,6 @@ import type { Faker } from '../..'; import { deprecated } from '../../internal/deprecated'; -import type { RecordKey } from './unique'; +import type { RecordKey } from '../helpers/unique'; /** * Module to generate unique entries. From b508580df68f36e514ff7e81b1a1449ef0048d1a Mon Sep 17 00:00:00 2001 From: xDivisionByZerox Date: Sat, 27 Aug 2022 22:34:06 +0200 Subject: [PATCH 4/4] chore(unique): update deprecation message --- src/modules/unique/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/unique/index.ts b/src/modules/unique/index.ts index f8fe1787e65..6c02d6d885e 100644 --- a/src/modules/unique/index.ts +++ b/src/modules/unique/index.ts @@ -60,8 +60,8 @@ export class Unique { } = {} ): ReturnType { deprecated({ - deprecated: 'faker.fake()', - proposed: 'faker.helpers.fake()', + deprecated: 'faker.unique()', + proposed: 'faker.helpers.unique()', since: '7.5', until: '8.0', });