Skip to content

Commit a7825a7

Browse files
ST-DDTCopilot
andauthored
refactor(core): expose core.locale as LocaleProxy (#3820)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9e2c0e3 commit a7825a7

7 files changed

Lines changed: 93 additions & 23 deletions

File tree

src/core.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { FakerConfig } from './config';
22
import type { LocaleDefinition } from './definitions';
3+
import type { LocaleProxy } from './internal/locale-proxy';
4+
import { createLocaleProxy } from './internal/locale-proxy';
35
import type { Randomizer } from './randomizer';
46
import { mergeLocales } from './utils/merge-locales';
57
import { generateMersenne53Randomizer } from './utils/mersenne';
@@ -13,7 +15,7 @@ export interface FakerCore {
1315
*
1416
* Always present, but it might be empty if the locale data is not available.
1517
*/
16-
readonly locale: LocaleDefinition;
18+
readonly locale: LocaleProxy;
1719

1820
/**
1921
* The randomizer used to generate random values.
@@ -32,7 +34,7 @@ export interface FakerOptions {
3234
*
3335
* @default {}
3436
*/
35-
locale?: LocaleDefinition | LocaleDefinition[];
37+
locale?: LocaleProxy | LocaleDefinition | LocaleDefinition[];
3638
/**
3739
* The randomizer used to generate random values.
3840
*
@@ -95,7 +97,9 @@ export function createFakerCore(options: FakerOptions = {}): FakerCore {
9597
}
9698

9799
return {
98-
locale: Array.isArray(locale) ? mergeLocales(locale) : locale,
100+
locale: createLocaleProxy(
101+
Array.isArray(locale) ? mergeLocales(locale) : locale
102+
),
99103
randomizer,
100104
config,
101105
};

src/faker.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { FakerOptions } from './core';
22
import type { LocaleDefinition, MetadataDefinition } from './definitions';
33
import { FakerError } from './errors/faker-error';
44
import type { LocaleProxy } from './internal/locale-proxy';
5-
import { createLocaleProxy } from './internal/locale-proxy';
65
import { AirlineModule } from './modules/airline';
76
import { AnimalModule } from './modules/animal';
87
import { BookModule } from './modules/book';
@@ -57,8 +56,6 @@ import { SimpleFaker } from './simple-faker';
5756
* customFaker.music.genre(); // throws Error as this data is not available in `es`
5857
*/
5958
export class Faker extends SimpleFaker {
60-
readonly definitions: LocaleProxy;
61-
6259
readonly airline: AirlineModule = new AirlineModule(this);
6360
readonly animal: AnimalModule = new AnimalModule(this);
6461
readonly book: BookModule = new BookModule(this);
@@ -85,6 +82,10 @@ export class Faker extends SimpleFaker {
8582
readonly word: WordModule = new WordModule(this);
8683

8784
get rawDefinitions(): LocaleDefinition {
85+
return this.fakerCore.locale.raw;
86+
}
87+
88+
get definitions(): LocaleProxy {
8889
return this.fakerCore.locale;
8990
}
9091

@@ -136,8 +137,6 @@ export class Faker extends SimpleFaker {
136137
'The locale option must contain at least one locale definition.'
137138
);
138139
}
139-
140-
this.definitions = createLocaleProxy(this.fakerCore.locale);
141140
}
142141

143142
/**
@@ -152,6 +151,6 @@ export class Faker extends SimpleFaker {
152151
* @since 8.1.0
153152
*/
154153
getMetadata(): MetadataDefinition {
155-
return this.fakerCore.locale.metadata ?? {};
154+
return this.fakerCore.locale.raw.metadata ?? {};
156155
}
157156
}

src/internal/locale-proxy.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
import type { LocaleDefinition } from '../definitions';
22
import { FakerError } from '../errors/faker-error';
33

4+
const LOCALE_PROXY_TAG = Symbol('FakerLocaleProxy');
5+
46
/**
57
* A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined.
68
*/
7-
export type LocaleProxy = Readonly<{
8-
[key in keyof LocaleDefinition]-?: LocaleProxyCategory<LocaleDefinition[key]>;
9-
}>;
9+
export type LocaleProxy = Readonly<
10+
{
11+
[key in keyof LocaleDefinition]-?: LocaleProxyCategory<
12+
LocaleDefinition[key]
13+
>;
14+
} & {
15+
/**
16+
* The raw locale definition used to create this proxy.
17+
* This can be useful to check if a category/entry exists without triggering the proxy's error.
18+
*/
19+
raw: LocaleDefinition;
20+
/**
21+
* Marker to identify a `LocaleProxy`.
22+
*/
23+
[LOCALE_PROXY_TAG]: true;
24+
}
25+
>;
1026

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

37+
/**
38+
* Checks if the given value is a LocaleProxy.
39+
*
40+
* @param value The value to check.
41+
*
42+
* @returns True if the value is a LocaleProxy, false otherwise.
43+
*/
44+
function isLocaleProxy(value: unknown): value is LocaleProxy {
45+
return (
46+
value != null &&
47+
typeof value === 'object' &&
48+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49+
(value as any)?.[LOCALE_PROXY_TAG] === true
50+
);
51+
}
52+
2153
/**
2254
* Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed.
2355
*
2456
* @param locale The locale definition to create the proxy for.
2557
*/
26-
export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy {
27-
const proxies = {} as LocaleDefinition;
58+
export function createLocaleProxy(
59+
locale: LocaleDefinition | LocaleProxy
60+
): LocaleProxy {
61+
if (isLocaleProxy(locale)) {
62+
return locale;
63+
}
64+
65+
const proxies = { raw: locale } as LocaleDefinition;
2866
return new Proxy(locale, {
2967
has(): true {
3068
// Categories are always present (proxied), that's why we return true.
@@ -33,25 +71,29 @@ export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy {
3371

3472
get(
3573
target: LocaleDefinition,
36-
categoryName: keyof LocaleDefinition
37-
): LocaleDefinition[keyof LocaleDefinition] {
38-
if (typeof categoryName === 'symbol' || categoryName === 'nodeType') {
74+
categoryName: keyof LocaleProxy
75+
): LocaleProxy[keyof LocaleProxy] {
76+
if (typeof categoryName === 'symbol') {
77+
if (categoryName === LOCALE_PROXY_TAG) {
78+
return true;
79+
}
80+
3981
return target[categoryName];
4082
}
4183

42-
if (categoryName in proxies) {
43-
return proxies[categoryName];
84+
if (categoryName === 'nodeType') {
85+
return target[categoryName];
4486
}
4587

46-
return (proxies[categoryName] = createCategoryProxy(
88+
return (proxies[categoryName] ??= createCategoryProxy(
4789
categoryName,
4890
target[categoryName]
4991
));
5092
},
5193

5294
set: throwReadOnlyError,
5395
deleteProperty: throwReadOnlyError,
54-
}) as LocaleProxy;
96+
}) as unknown as LocaleProxy;
5597
}
5698

5799
/**

src/modules/helpers/eval.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/;
6666
export function fakeEval(
6767
expression: string,
6868
faker: Faker,
69-
entrypoints: ReadonlyArray<unknown> = [faker, faker.fakerCore.locale]
69+
entrypoints: ReadonlyArray<unknown> = [faker, faker.definitions.raw]
7070
): unknown {
7171
if (expression.length === 0) {
7272
throw new FakerError('Eval expression cannot be empty.');

src/modules/person/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class PersonModule extends ModuleBase {
137137
* @since 8.0.0
138138
*/
139139
lastName(sex?: SexType): string {
140-
const patterns = this.faker.fakerCore.locale.person?.last_name_pattern;
140+
const patterns = this.faker.definitions.raw.person?.last_name_pattern;
141141
if (patterns != null) {
142142
const pattern = this.faker.helpers.weightedArrayElement(
143143
selectDefinition(this.faker, sex, patterns)

test/core.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
22
import type { FakerConfig } from '../src/config';
33
import { createFakerCore } from '../src/core';
44
import type { LocaleDefinition } from '../src/definitions/definitions';
5+
import { createLocaleProxy } from '../src/internal/locale-proxy';
56
import type { Randomizer } from '../src/randomizer';
67
import { generateMersenne53Randomizer } from '../src/utils/mersenne';
78

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

4647
expect(actual.locale).toEqual({ ...locale1, ...locale2 });
4748
});
49+
50+
it('should handle LocaleProxy', () => {
51+
const locale: LocaleDefinition = { test1: { test: 'test1' } };
52+
const proxy = createLocaleProxy(locale);
53+
const actual = createFakerCore({ locale: proxy });
54+
55+
expect(actual.locale).toBe(proxy);
56+
expect(actual.locale).toEqual(locale);
57+
expect(actual.locale.raw).toBe(locale);
58+
});
4859
});
4960

5061
describe('randomizer', () => {

test/internal/locale-proxy.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ describe('LocaleProxy', () => {
1414
it('should be possible to use not equals on locale', () => {
1515
expect(locale).not.toEqual(createLocaleProxy({}));
1616
});
17+
18+
it('should be possible to pass a LocaleProxy to createLocaleProxy', () => {
19+
const proxy = createLocaleProxy(locale);
20+
21+
expect(proxy).toBe(locale);
22+
});
23+
24+
it('should be possible to access raw without throwing', () => {
25+
expect(locale.raw.missing?.missing).toBeUndefined();
26+
});
27+
28+
it('should expose the original locale definition via raw', () => {
29+
expect(locale.raw).toBe(en);
30+
});
1731
});
1832

1933
describe('category', () => {

0 commit comments

Comments
 (0)