Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 7 additions & 3 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { FakerConfig } from './config';
import type { LocaleDefinition } from './definitions';
import type { LocaleProxy } from './internal/locale-proxy';
import { createLocaleProxy } from './internal/locale-proxy';
import type { Randomizer } from './randomizer';
import { mergeLocales } from './utils/merge-locales';
import { generateMersenne53Randomizer } from './utils/mersenne';
Expand All @@ -13,7 +15,7 @@ export interface FakerCore {
*
* Always present, but it might be empty if the locale data is not available.
*/
readonly locale: LocaleDefinition;
readonly locale: LocaleProxy;

/**
* The randomizer used to generate random values.
Expand All @@ -32,7 +34,7 @@ export interface FakerOptions {
*
* @default {}
*/
locale?: LocaleDefinition | LocaleDefinition[];
locale?: LocaleProxy | LocaleDefinition | LocaleDefinition[];
Comment thread
xDivisionByZerox marked this conversation as resolved.
/**
* The randomizer used to generate random values.
*
Expand Down Expand Up @@ -95,7 +97,9 @@ export function createFakerCore(options: FakerOptions = {}): FakerCore {
}

return {
locale: Array.isArray(locale) ? mergeLocales(locale) : locale,
locale: createLocaleProxy(
Array.isArray(locale) ? mergeLocales(locale) : locale
),
randomizer,
config,
};
Expand Down
11 changes: 5 additions & 6 deletions src/faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { FakerOptions } from './core';
import type { LocaleDefinition, MetadataDefinition } from './definitions';
import { FakerError } from './errors/faker-error';
import type { LocaleProxy } from './internal/locale-proxy';
import { createLocaleProxy } from './internal/locale-proxy';
import { AirlineModule } from './modules/airline';
import { AnimalModule } from './modules/animal';
import { BookModule } from './modules/book';
Expand Down Expand Up @@ -57,8 +56,6 @@ import { SimpleFaker } from './simple-faker';
* customFaker.music.genre(); // throws Error as this data is not available in `es`
*/
export class Faker extends SimpleFaker {
readonly definitions: LocaleProxy;

readonly airline: AirlineModule = new AirlineModule(this);
readonly animal: AnimalModule = new AnimalModule(this);
readonly book: BookModule = new BookModule(this);
Expand All @@ -85,6 +82,10 @@ export class Faker extends SimpleFaker {
readonly word: WordModule = new WordModule(this);

get rawDefinitions(): LocaleDefinition {
return this.fakerCore.locale.raw;
}

get definitions(): LocaleProxy {
return this.fakerCore.locale;
}

Comment thread
ST-DDT marked this conversation as resolved.
Expand Down Expand Up @@ -136,8 +137,6 @@ export class Faker extends SimpleFaker {
'The locale option must contain at least one locale definition.'
);
}

this.definitions = createLocaleProxy(this.fakerCore.locale);
}

/**
Expand All @@ -152,6 +151,6 @@ export class Faker extends SimpleFaker {
* @since 8.1.0
*/
getMetadata(): MetadataDefinition {
return this.fakerCore.locale.metadata ?? {};
return this.fakerCore.locale.raw.metadata ?? {};
}
}
66 changes: 54 additions & 12 deletions src/internal/locale-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import type { LocaleDefinition } from '../definitions';
import { FakerError } from '../errors/faker-error';

const LOCALE_PROXY_TAG = Symbol('FakerLocaleProxy');

/**
* A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined.
*/
export type LocaleProxy = Readonly<{
[key in keyof LocaleDefinition]-?: LocaleProxyCategory<LocaleDefinition[key]>;
}>;
export type LocaleProxy = Readonly<
{
[key in keyof LocaleDefinition]-?: LocaleProxyCategory<
LocaleDefinition[key]
>;
} & {
/**
* The raw locale definition used to create this proxy.
* This can be useful to check if a category/entry exists without triggering the proxy's error.
*/
raw: LocaleDefinition;
Comment thread
xDivisionByZerox marked this conversation as resolved.
Comment thread
ST-DDT marked this conversation as resolved.
/**
* Marker to identify a `LocaleProxy`.
*/
[LOCALE_PROXY_TAG]: true;
}
>;

type LocaleProxyCategory<T> = Readonly<{
[key in keyof T]-?: LocaleProxyEntry<T[key]>;
Expand All @@ -18,13 +34,35 @@ const throwReadOnlyError: () => never = () => {
throw new FakerError('You cannot edit the locale data on the faker instance');
};

/**
* Checks if the given value is a LocaleProxy.
*
* @param value The value to check.
*
* @returns True if the value is a LocaleProxy, false otherwise.
*/
function isLocaleProxy(value: unknown): value is LocaleProxy {
return (
value != null &&
typeof value === 'object' &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(value as any)?.[LOCALE_PROXY_TAG] === true
);
}
Comment thread
ST-DDT marked this conversation as resolved.

/**
* Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed.
*
* @param locale The locale definition to create the proxy for.
*/
export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy {
const proxies = {} as LocaleDefinition;
export function createLocaleProxy(
locale: LocaleDefinition | LocaleProxy
): LocaleProxy {
if (isLocaleProxy(locale)) {
return locale;
}

const proxies = { raw: locale } as LocaleDefinition;
return new Proxy(locale, {
Comment thread
ST-DDT marked this conversation as resolved.
has(): true {
// Categories are always present (proxied), that's why we return true.
Expand All @@ -33,25 +71,29 @@ export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy {

get(
target: LocaleDefinition,
categoryName: keyof LocaleDefinition
): LocaleDefinition[keyof LocaleDefinition] {
if (typeof categoryName === 'symbol' || categoryName === 'nodeType') {
categoryName: keyof LocaleProxy
Comment thread
ST-DDT marked this conversation as resolved.
): LocaleProxy[keyof LocaleProxy] {
if (typeof categoryName === 'symbol') {
if (categoryName === LOCALE_PROXY_TAG) {
return true;
}

return target[categoryName];
Comment thread
ST-DDT marked this conversation as resolved.
}

if (categoryName in proxies) {
return proxies[categoryName];
if (categoryName === 'nodeType') {
Comment thread
xDivisionByZerox marked this conversation as resolved.
return target[categoryName];
}

return (proxies[categoryName] = createCategoryProxy(
return (proxies[categoryName] ??= createCategoryProxy(
categoryName,
target[categoryName]
));
},

set: throwReadOnlyError,
deleteProperty: throwReadOnlyError,
}) as LocaleProxy;
}) as unknown as LocaleProxy;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/modules/helpers/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/;
export function fakeEval(
expression: string,
faker: Faker,
entrypoints: ReadonlyArray<unknown> = [faker, faker.fakerCore.locale]
entrypoints: ReadonlyArray<unknown> = [faker, faker.definitions.raw]
): unknown {
if (expression.length === 0) {
throw new FakerError('Eval expression cannot be empty.');
Expand Down
2 changes: 1 addition & 1 deletion src/modules/person/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class PersonModule extends ModuleBase {
* @since 8.0.0
*/
lastName(sex?: SexType): string {
const patterns = this.faker.fakerCore.locale.person?.last_name_pattern;
const patterns = this.faker.definitions.raw.person?.last_name_pattern;
if (patterns != null) {
const pattern = this.faker.helpers.weightedArrayElement(
selectDefinition(this.faker, sex, patterns)
Expand Down
11 changes: 11 additions & 0 deletions test/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { FakerConfig } from '../src/config';
import { createFakerCore } from '../src/core';
import type { LocaleDefinition } from '../src/definitions/definitions';
import { createLocaleProxy } from '../src/internal/locale-proxy';
import type { Randomizer } from '../src/randomizer';
import { generateMersenne53Randomizer } from '../src/utils/mersenne';

Expand Down Expand Up @@ -45,6 +46,16 @@ describe('createFakerCore', () => {

expect(actual.locale).toEqual({ ...locale1, ...locale2 });
});

it('should handle LocaleProxy', () => {
const locale: LocaleDefinition = { test1: { test: 'test1' } };
const proxy = createLocaleProxy(locale);
const actual = createFakerCore({ locale: proxy });

expect(actual.locale).toBe(proxy);
expect(actual.locale).toEqual(locale);
expect(actual.locale.raw).toBe(locale);
});
});

describe('randomizer', () => {
Expand Down
14 changes: 14 additions & 0 deletions test/internal/locale-proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ describe('LocaleProxy', () => {
it('should be possible to use not equals on locale', () => {
expect(locale).not.toEqual(createLocaleProxy({}));
});

it('should be possible to pass a LocaleProxy to createLocaleProxy', () => {
const proxy = createLocaleProxy(locale);

expect(proxy).toBe(locale);
});
Comment thread
ST-DDT marked this conversation as resolved.

it('should be possible to access raw without throwing', () => {
expect(locale.raw.missing?.missing).toBeUndefined();
});

it('should expose the original locale definition via raw', () => {
expect(locale.raw).toBe(en);
});
});

describe('category', () => {
Expand Down
Loading