diff --git a/src/definitions/definitions.ts b/src/definitions/definitions.ts index d59f84fc859..b696d47a2ff 100644 --- a/src/definitions/definitions.ts +++ b/src/definitions/definitions.ts @@ -69,7 +69,8 @@ export type LocaleDefinition = { [module in keyof Definitions]?: Partial; } & { // Unsupported & custom modules - [group: string]: Record | string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [group: string]: any; }; /** @@ -78,8 +79,8 @@ export type LocaleDefinition = { * that don't require prior getter generation in the future. */ export type DefinitionTypes = { - readonly title: string; - readonly separator: string; + readonly title: 'metadata'; + readonly separator: 'metadata'; } & { readonly [module in keyof Definitions]: Array; }; @@ -89,8 +90,8 @@ export type DefinitionTypes = { * that needs to have a fallback generated in Faker.loadDefinitions(). */ export const DEFINITIONS: DefinitionTypes = { - title: '', - separator: '', + title: 'metadata', + separator: 'metadata', address: ADDRESS, animal: ANIMAL, diff --git a/src/faker.ts b/src/faker.ts index 7b74d58c9ac..14cb107d36f 100644 --- a/src/faker.ts +++ b/src/faker.ts @@ -47,8 +47,7 @@ export class Faker { locale: UsableLocale; localeFallback: UsableLocale; - // Will be lazy init - readonly definitions: LocaleDefinition = {} as LocaleDefinition; + readonly definitions: LocaleDefinition = this.initDefinitions(); seedValue?: number | number[]; @@ -98,43 +97,56 @@ export class Faker { this.locales = opts.locales; this.locale = opts.locale || 'en'; this.localeFallback = opts.localeFallback || 'en'; - - this.loadDefinitions(); } /** - * Load the definitions contained in the locales file for the given types. - * - * Background: Certain localization sets contain less data then others. - * In the case of a missing definition, use the localeFallback's values - * to substitute the missing data. + * Creates a Proxy based LocaleDefinition that virtually merges the locales. */ - private loadDefinitions(): void { - // TODO @Shinigami92 2022-01-11: Find a way to load this even more dynamically - // In a way so that we don't accidentally miss a definition - for (const [moduleName, entryNames] of Object.entries(DEFINITIONS)) { - if (typeof entryNames === 'string') { - // For 'title' and 'separator' - Object.defineProperty(this.definitions, moduleName, { - get: (): unknown /* string */ => - this.locales[this.locale][moduleName] ?? - this.locales[this.localeFallback][moduleName], - }); - continue; - } - - if (this.definitions[moduleName] == null) { - this.definitions[moduleName] = {}; + private initDefinitions(): LocaleDefinition { + // Returns the first LocaleDefinition[key] in any locale + const resolveBaseData = (key: keyof LocaleDefinition): unknown => + this.locales[this.locale][key] ?? this.locales[this.localeFallback][key]; + + // Returns the first LocaleDefinition[module][entry] in any locale + const resolveModuleData = ( + module: keyof LocaleDefinition, + entry: string + ): unknown => + this.locales[this.locale][module]?.[entry] ?? + this.locales[this.localeFallback][module]?.[entry]; + + // Returns a proxy that can return the entries for a module (if it exists) + const moduleLoader = ( + module: keyof LocaleDefinition + ): Record | undefined => { + if (resolveBaseData(module)) { + return new Proxy( + {}, + { + get(target, entry: string): unknown { + return resolveModuleData(module, entry); + }, + } + ); + } else { + return undefined; } - - for (const entryName of entryNames) { - Object.defineProperty(this.definitions[moduleName], entryName, { - get: (): unknown => - this.locales[this.locale][moduleName]?.[entryName] ?? - this.locales[this.localeFallback][moduleName]?.[entryName], - }); - } - } + }; + + return new Proxy({} as LocaleDefinition, { + get(target: LocaleDefinition, module: string): unknown { + let result = target[module]; + if (result) { + return result; + } else if (DEFINITIONS[module] === 'metadata') { + return resolveBaseData(module); + } else { + result = moduleLoader(module); + target[module] = result; + return result; + } + }, + }); } seed(seed?: number | number[]): void { diff --git a/test/faker.spec.ts b/test/faker.spec.ts index 983a31bd9b4..d8ca61c8094 100644 --- a/test/faker.spec.ts +++ b/test/faker.spec.ts @@ -31,24 +31,37 @@ describe('faker', () => { ); }); - describe('title', () => { - it.each(Object.keys(faker.locales))('title (%s)', (locale) => { - faker.locale = locale; - expect(faker.definitions.title).toBe(faker.locales[locale].title); + describe('definitions', () => { + describe('title', () => { + it.each(Object.keys(faker.locales))('title (%s)', (locale) => { + faker.locale = locale; + expect(faker.definitions.title).toBe(faker.locales[locale].title); + }); }); - }); - describe('separator', () => { - it.each(Object.keys(faker.locales))('separator (%s)', (locale) => { - faker.locale = locale; - expect(faker.definitions.separator).toBeTypeOf('string'); + describe('separator', () => { + it.each(Object.keys(faker.locales))('separator (%s)', (locale) => { + faker.locale = locale; + expect(faker.definitions.separator).toBeTypeOf('string'); + }); + + it('separator (with fallback)', () => { + // Use a language that doesn't have a separator specified + expect(faker.locales['en_US'].separator).toBeUndefined(); + // Check that the fallback works + expect(faker.definitions.separator).toBe(faker.locales['en'].separator); + }); }); - it('separator (with fallback)', () => { - // Use a language that doesn't have a separator specified - expect(faker.locales['en_US'].separator).toBeUndefined(); - // Check that the fallback works - expect(faker.definitions.separator).toBe(faker.locales['en'].separator); + it('locale definition accessability', () => { + // Metadata + expect(faker.definitions.title).toBeDefined(); + // Standard modules + expect(faker.definitions.address.city_name).toBeDefined(); + // Custom modules + expect(faker.definitions.business.credit_card_types).toBeDefined(); + expect(faker.definitions.missing).toBeUndefined(); + expect(faker.definitions.business.missing).toBeUndefined(); }); });