Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce locale proxy #2004

Merged
merged 46 commits into from
Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f688434
feat: introduce locale access proxy
ST-DDT Apr 1, 2023
9db8ff9
chore: fix typo
ST-DDT Apr 2, 2023
bef53d7
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 2, 2023
b495965
test: extend proxy tests
ST-DDT Apr 2, 2023
c0be7e1
chore: fix import order
ST-DDT Apr 2, 2023
109ef31
test: use describe instead of comment sections
ST-DDT Apr 2, 2023
0f42fda
chore: apply suggestions
ST-DDT Apr 2, 2023
8530492
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 2, 2023
149ec6b
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 2, 2023
eebccd0
docs: extend migration guide
ST-DDT Apr 2, 2023
5c70e32
chore: typo
ST-DDT Apr 2, 2023
5c73725
chore: reword
ST-DDT Apr 2, 2023
df38ac7
chore: mark string type
ST-DDT Apr 2, 2023
d8fa41f
docs: add examples
ST-DDT Apr 2, 2023
05d2cab
docs: improve example
ST-DDT Apr 2, 2023
79cb31a
docs: apply suggestions
ST-DDT Apr 2, 2023
885627b
chore: fix typo
ST-DDT Apr 2, 2023
0ff62af
chore: apply suggestion
ST-DDT Apr 2, 2023
42443fa
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 2, 2023
419edfe
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 4, 2023
e0b8ac2
test: remove expected failure
ST-DDT Apr 4, 2023
fff9ccb
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 5, 2023
fe435ad
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 12, 2023
021e851
chore: apply suggestions
ST-DDT Apr 12, 2023
f69b474
chore: lost commit
ST-DDT Apr 12, 2023
d2a5312
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 13, 2023
dec1659
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 15, 2023
2a4cc3f
chore: apply suggestions
ST-DDT Apr 15, 2023
372e1f2
chore: apply suggestion
ST-DDT Apr 15, 2023
fd80d4f
test: update error message
ST-DDT Apr 15, 2023
4b92071
Update test/faker.spec.ts
ST-DDT Apr 15, 2023
385d4fa
chore: format
ST-DDT Apr 15, 2023
f107258
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 16, 2023
9b298c7
test: add tests for Object.keys
ST-DDT Apr 17, 2023
316941d
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 20, 2023
f99f72c
fix: proxy-impl
ST-DDT Apr 20, 2023
106bf70
Apply suggestions from code review
ST-DDT Apr 20, 2023
d722c6a
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 21, 2023
fbde3ac
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 21, 2023
24e14cf
refactor: use null as not applicable data
ST-DDT Apr 21, 2023
6acf6de
Merge branch 'next' into feat/locale-access-proxy
matthewmayer Apr 22, 2023
aafe702
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 22, 2023
1f29440
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 23, 2023
2ab6023
chore: own review
ST-DDT Apr 23, 2023
783b7ba
Update src/locale-proxy.ts
ST-DDT Apr 23, 2023
f915d49
Merge branch 'next' into feat/locale-access-proxy
ST-DDT Apr 23, 2023
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
42 changes: 42 additions & 0 deletions docs/guide/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,48 @@ for (let user of users) {

For more information refer to our [Localization Guide](localization).

### For missing locale data, Faker will now throw instead of returning `undefined` or `a`-`c`

::: note Note
The following section mostly applies to custom-built Faker instances.
:::

Previously, for example if `en` didn't have data for `animal.cat`, then `faker.animal.cat()` would have returned one of `a`, `b` or `c` (`arrayElement`'s default value).
These values aren't expected/useful as a fallback and potentially also violate the method's defined return type definitions (in case it doesn't return a `string`).

We have now addressed this by changing the implementation so that an error is thrown, prompting you to provide/contribute the missing data.
This will also give you detailed information which data are missing.
If you want to check for data you can either use `entry in faker.definitions.category` or use `faker.rawDefinitions.category?.entry` instead.

```ts
import { Faker, fakerES, es } from '@faker-js/faker';

const fakerES_noFallbacks = new Faker({
locale: [es],
});
fakerES.music.songName(); // 'I Want to Hold Your Hand' (fallback from en)
// Previously:
//fakerES_noFallbacks.music.songName(); // 'b'
// Now:
fakerES_noFallbacks.music.songName(); // throws a FakerError
```

This also has an impact on data that aren't applicable to a locale, for example Chinese doesn't use prefixes in names.

```ts
import { faker, fakerZH_CN, zh_CN } from '@faker-js/faker';

const fakerZH_CN_noFallbacks = new Faker({
locale: [zh_CN],
});

faker.name.prefix(); // 'Mr'
// Previously:
//fakerZH_CN_noFallbacks.person.prefix(); // undefined
// Now:
fakerZH_CN.person.prefix(); // throws a FakerError
```

ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
### `faker.mersenne` and `faker.helpers.repeatString` removed

`faker.mersenne` and `faker.helpers.repeatString` were only ever intended for internal use, and are no longer available.
Expand Down
2 changes: 1 addition & 1 deletion src/definitions/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ export type LocaleDefinition = {
system?: SystemDefinitions;
vehicle?: VehicleDefinitions;
word?: WordDefinitions;
} & Record<string, Record<string, unknown>>;
} & Record<string, Record<string, unknown> | undefined>;
8 changes: 6 additions & 2 deletions src/faker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { FakerError } from './errors/faker-error';
import { deprecated } from './internal/deprecated';
import type { Mersenne } from './internal/mersenne/mersenne';
import mersenne from './internal/mersenne/mersenne';
import type { LocaleProxy } from './locale-proxy';
import { createLocaleProxy } from './locale-proxy';
import { AirlineModule } from './modules/airline';
import { AnimalModule } from './modules/animal';
import { ColorModule } from './modules/color';
Expand Down Expand Up @@ -59,7 +61,8 @@ import { mergeLocales } from './utils/merge-locales';
* customFaker.music.genre(); // throws Error as this data is not available in `es`
*/
export class Faker {
readonly definitions: LocaleDefinition;
readonly rawDefinitions: LocaleDefinition;
readonly definitions: LocaleProxy;
private _defaultRefDate: () => Date = () => new Date();

/**
Expand Down Expand Up @@ -329,7 +332,8 @@ export class Faker {
locale = mergeLocales(locale);
}

this.definitions = locale as LocaleDefinition;
this.rawDefinitions = locale as LocaleDefinition;
this.definitions = createLocaleProxy(this.rawDefinitions);
}

/**
Expand Down
91 changes: 91 additions & 0 deletions src/locale-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { LocaleDefinition } from './definitions';
import { FakerError } from './errors/faker-error';

/**
* A proxy for LocaleDefinitions 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]-?: Readonly<
Required<NonNullable<LocaleDefinition[key]>>
>;
}>;

const throwReadOnlyError: () => never = () => {
throw new FakerError('You cannot edit the locale data on the faker instance');
};

/**
* 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;
return new Proxy(locale, {
has(): true {
// Categories are always present (proxied), that's why we return true.
return true;
},

get(
target: LocaleDefinition,
categoryName: keyof LocaleDefinition
): LocaleDefinition[keyof LocaleDefinition] {
if (categoryName in proxies) {
return proxies[categoryName];
}

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

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

/**
* Creates a proxy for a category that throws an error when accessing an undefined property.
*
* @param categoryName The name of the category.
* @param categoryData The module to create the proxy for.
*/
function createCategoryProxy<
CategoryData extends Record<string | symbol, unknown>
>(
categoryName: string,
categoryData: CategoryData = {} as CategoryData
): Required<CategoryData> {
return new Proxy(categoryData, {
has(target: CategoryData, entryName: keyof CategoryData): boolean {
const value = target[entryName];
return value != null;
},

get(
target: CategoryData,
entryName: keyof CategoryData
): CategoryData[keyof CategoryData] {
const value = target[entryName];
if (value === null) {
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
throw new FakerError(
`The locale data for '${categoryName}.${entryName.toString()}' aren't applicable to this locale.
If you think this is a bug, please report it at: https://github.com/faker-js/faker`
);
} else if (value === undefined) {
throw new FakerError(
`The locale data for '${categoryName}.${entryName.toString()}' are missing in this locale.
Please contribute the missing data to the project or use a locale/Faker instance that has these data.
For more information see https://next.fakerjs.dev/guide/localization.html`
);
} else {
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
return value;
}
},

set: throwReadOnlyError,
deleteProperty: throwReadOnlyError,
}) as Required<CategoryData>;
}
2 changes: 1 addition & 1 deletion src/modules/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,7 @@ export class HelpersModule {
const parts = method.split('.');

let currentModuleOrMethod: unknown = this.faker;
let currentDefinitions: unknown = this.faker.definitions;
let currentDefinitions: unknown = this.faker.rawDefinitions;
xDivisionByZerox marked this conversation as resolved.
Show resolved Hide resolved

// Search for the requested method or definition
for (const part of parts) {
Expand Down
3 changes: 1 addition & 2 deletions src/modules/location/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ export class LocationModule {
const { state } = options;

if (state) {
const zipRange =
this.faker.definitions.location.postcode_by_state?.[state];
const zipRange = this.faker.definitions.location.postcode_by_state[state];

if (zipRange) {
return String(this.faker.number.int(zipRange));
Expand Down
8 changes: 4 additions & 4 deletions src/modules/person/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class PersonModule {
*/
firstName(sex?: SexType): string {
const { first_name, female_first_name, male_first_name } =
this.faker.definitions.person;
this.faker.rawDefinitions.person ?? {};

return selectDefinition(this.faker, this.faker.helpers.arrayElement, sex, {
generic: first_name,
Expand Down Expand Up @@ -132,7 +132,7 @@ export class PersonModule {
last_name_pattern,
male_last_name_pattern,
female_last_name_pattern,
} = this.faker.definitions.person;
} = this.faker.rawDefinitions.person ?? {};

if (
last_name_pattern != null ||
Expand Down Expand Up @@ -174,7 +174,7 @@ export class PersonModule {
*/
middleName(sex?: SexType): string {
const { middle_name, female_middle_name, male_middle_name } =
this.faker.definitions.person;
this.faker.rawDefinitions.person ?? {};

return selectDefinition(this.faker, this.faker.helpers.arrayElement, sex, {
generic: middle_name,
Expand Down Expand Up @@ -315,7 +315,7 @@ export class PersonModule {
*/
prefix(sex?: SexType): string {
const { prefix, female_prefix, male_prefix } =
this.faker.definitions.person;
this.faker.rawDefinitions.person ?? {};

return selectDefinition(this.faker, this.faker.helpers.arrayElement, sex, {
generic: prefix,
Expand Down
1 change: 1 addition & 0 deletions test/all_functional.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { allLocales, Faker, RandomModule } from '../src';
import { allFakers, fakerEN } from '../src';

const IGNORED_MODULES = [
'rawDefinitions',
'definitions',
'helpers',
'_mersenne',
Expand Down
23 changes: 20 additions & 3 deletions test/faker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,33 @@ describe('faker', () => {
}
});

describe('rawDefinitions', () => {
it('locale rawDefinition accessibility', () => {
// Metadata
expect(faker.rawDefinitions.metadata.title).toBeDefined();
// Standard modules
expect(faker.rawDefinitions.location?.city_name).toBeDefined();
// Non-existing module
expect(faker.rawDefinitions.missing).toBeUndefined();
// Non-existing definition in a non-existing module
expect(faker.rawDefinitions.missing?.missing).toBeUndefined();
// Non-existing definition in an existing module
expect(faker.rawDefinitions.location?.missing).toBeUndefined();
});
});

describe('definitions', () => {
it('locale definition accessability', () => {
it('locale definition accessibility', () => {
// Metadata
expect(faker.definitions.metadata.title).toBeDefined();
// Standard modules
expect(faker.definitions.location.city_name).toBeDefined();
// Non-existing module
expect(faker.definitions.missing).toBeUndefined();
expect(faker.definitions.missing).toBeDefined();
// Non-existing definition in a non-existing module
expect(() => faker.definitions.missing.missing).toThrow();
// Non-existing definition in an existing module
expect(faker.definitions.location.missing).toBeUndefined();
expect(() => faker.definitions.location.missing).toThrow();
});
});

Expand Down
Loading