-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Clear and concise description of the problem
Currently the faker methods are organized in modules and these are joined to the whole Faker class.
Most methods in the modules reference a function in a different module, by invoking it via the embedded Faker reference.
This makes it near impossible for building tools to drop unused parts of code package.
Changing the implementation to use standalone methods, that only call other standalone methods, mostly solves this issue.
Old code/syntax (e.g. faker.person.firstName) must remain valid.
Suggested solution
1. Extract the functions from the module classes into their own files (the same applies to internal helper methods)
- modules
- airline
- airline.ts
- airport.ts
- ...
- helpers
- arrayElement.ts
- arrayElements.ts
- multiple.ts
- ...
- ..
2. Change the signature of the methods, so that it replaces it the reference to the main Faker
instance e.g. by passing it as argument.
function firstName(fakerCore: FakerCore, ...remainingArgs) {
return arrayElement(fakerCore, fakerCore.definitions.person.firstName);
}
fakerCore ≈> Faker without any modules (only definitions, randomizer, config)
3. Call the standalone functions from the modules.
export function newPersonModule(fakerCore: FakerCore): PersonModule {
return ({
firstName: (..args) => firstName(fakerCore, ...args);
...
});
}
4. Optionally, "process" the function to make it easier to use with the same/a fakerCore.
export function fakerize<T extends (fakerCore: FakerCore, ...args: any[]) => any>( fn: T ): FakerizedMethod<T> {
const fakerized = fn as FakerizedMethod<T>;
fakerized.withCore = (fakerCore: FakerCore) => (...args: FakerizedParameters<T>) => fn(fakerCore, ...args);
// Potentially more methods
return fakerized;
}
// We need to check whether this negatively impacts readability of the type if not inlined
type FakerizedMethod<T extends () => any> = T & Readonly<{
withCore: ( fakerCore: FakerCore ) => (...args: FakerizedParameters<T>) => ReturnType<T>;
}>;
type FakerizedParameters<T extends (...args: any[]) => any> =
Parameters<T> extends [FakerCore, ...infer U] ? U : never;
Usage:
// in firstName.ts
export const firstName = fakerize(firstName);
// by users or maybe pre-built per locale
export const enFirstName = firstName.withCore(fakerCoreEN);
export function newPersonModule(fakerCore: FakerCore): PersonModule {
return ({
firstName: firstName.withCore(fakerCore);
...
});
}
// by users
enFirstName() // Henry
Usage nano bound standalone module function
// in locales/en.ts
const nanoCoreFirstName: FakerCore = { randomizer, config, locale: { person: { first_name }}}; // Contains only the required locale data for firstName
export const enFirstName = firstName.withCore(nanoCoreFirstName);
// by users
enFirstName() // Henry
The nano bound standalone module function contain only the data that are needed for that specific function and nothing else.
So they are tiny after treeshaking 1-10KB instead of the full 600KB for en.
Pros:
- Allows adding meta functions, such as
firstName.isAvailable = (fakerCore) => fakerCore.rawDefinitions.person.firstName != null;
Cons:
- The hover-able API docs are slightly less beautiful because it is a
export const
instead ofexport function
.
Alternative
I also considered making the fakerCore
parameter the last parameter and optional by defaulting to e.g. fakerCoreEN
.
However that has a few disadvantages:
- The default in the signature or otherwise is likely to force all of the English locale to be bundled, even if it is never used.
- The optional nature of the parameter makes it easy to forget especially when creating complex objects in a function with a fakerCore parameter itself.
// This method can be used by multiple languages
function mockPerson(fakerCore?: FakerCore, static?: Partial<Person> = {}): Person {
return ({
id: static.id ?? uuid(fakerCore),
firstName: static.firstName ?? firstName(),
lastName: static.lastName ?? lastName(),
});
}
// Did you spot the missing parameters?
// Would you if I didn't tell you?
// If it was more complicated?
// Or if this method is only a nested function within the `mockLeasingAgreement()` function?
- If the parameter is the last one, you will likely end with some undefined parameters in between.
firstName(undefined (SexType), fakerCoreFR)
- If the parameter is last, there is also additional work to do when processing the function.
export function fakerize<T extends (...args: any[], fakerCore: FakerCore) => any>(
fn: T,
fakerCoreParameterIndex: number // <-- probably not compile time guarded => hard to use by our users
): FakerizedMethod<T> {
const fakerized = fn as FakerizedMethod<T>;
fakerized.withCore = (fakerCore: FakerCore) => (...args: FakerizedParameters<T>) => {
args[fakerCoreParameterIndex] = fakerCore;
fn(...args);
}
return fakerized;
}
- Having the fakerCore parameter last makes it very hard for methods like
multiple
to pass the argument on.
This basically prevents using derive in combination with pass-through parameters (IIRC currently only supported by unique)
Functions with pass-through parameters might result in easier to read code, but that belongs into a different proposal
function multiple(source: (...args: A[], fakerCore) => T, options, fakerCore: FakerCore, ...args: A[]) {
// Impossible
}
multiple(firstName, { count: 5}, fakerCoreEN, 'male');
Additional context
This is an attempt at detailing the proposal made in #1250
It was made with a change like #2664 in mind to retain optimal performance.
This proposal will not remove or deprecate the well known faker.person.firstName
syntax.
Adopting this proposal in combination with #2664 has side effects for methods that accept other methods as parameters, as they have to pass on the fakerCore instance.
function multiple(fakerCore: FakerCore, source: (fakerCore: FakerCore) => T, options) {
cosnt derived = fakerCore.derive();
return Array.from({length: options.count}, () => source(derived));
}
If the user does not use the derived instance, then they use more seeds from their main instance and might even use different fakerCores for the multiple function and the source function.
// BAD
multiple(fakerCoreEN, () => firstName(fakerCoreFR), { count: 3 });
// GOOD
multiple(fakerCoreEN, (core) => firstName(core), { count: 3 });
// BETTER?
multiple(fakerCoreEN, firstName, { count: 3 });
This also affects other functions such as uniqueArray
and unique
.
Aka that extends the scope of faker to some (helper) functions that call faker functions indirectly.
Note: I cut quite a few related/derived proposals from this issue entirely, because it is long enough as is:
- Nano-Localized-Functions for min bundle sizes
- Most of the Fakerize Meta-Framework
- Code-Transformer
- Most of the extension framework
- Implementation details
- generateComplex(fakerCore, { key: keyof T: (fakerCore) => T[key] }): T
- ...
I just though about these while writing the ticket, thus it does not imply that I actually plan on adding any of these.
(I likely forgot a few as well, because I only wrote them down at the end😉.)
Usage Example
const usersEN = mockUsers(fakerCoreEN);
const usersFR = mockUsers(fakerCoreFR);
function mockUsers(fakerCore: FakerCore) {
return multiple(fakerCore, mockUser, { count: 3 });
}
function mockUser(fakerCore: FakerCore): User{
return ({
id: uuid(fakerCore),
firstName: firstName(fakerCore),
lastName: lastName(fakerCore),
});
}