diff --git a/packages/generate/src/field-rules.ts b/packages/generate/src/field-rules.ts index 2d8fe6f..02f9976 100644 --- a/packages/generate/src/field-rules.ts +++ b/packages/generate/src/field-rules.ts @@ -47,8 +47,13 @@ function normalize(fieldName: string): string { /** * String field rules, in precedence order. Date-ness runs first so fields * like `emailVerifiedAt` produce a date rather than an email; username and - * filename run before the generic person-name rule so `userName`/`fileName` + * filename run before the person-name rules so `userName`/`fileName` * (which contain the word "name") aren't mistaken for a person's name. + * + * Person names are split per variant: `firstName` gets a given name, + * `lastName` a family name, and only `name`/`fullName`/`displayName` get a + * full name — a pasted `User` model is the most common input there is, and + * `{ firstName: "Claudine Kuhn" }` reads as wrong at a glance. */ export const STRING_RULES: FieldRule[] = [ { @@ -72,9 +77,18 @@ export const STRING_RULES: FieldRule[] = [ use: (faker) => `${faker.lorem.slug()}.txt`, }, { - name: 'person', - when: (n) => - ['name', 'firstname', 'lastname', 'fullname', 'displayname'].includes(normalize(n)), + name: 'first-name', + when: (n) => ['firstname', 'givenname', 'forename'].includes(normalize(n)), + use: (faker) => faker.person.firstName(), + }, + { + name: 'last-name', + when: (n) => ['lastname', 'surname', 'familyname'].includes(normalize(n)), + use: (faker) => faker.person.lastName(), + }, + { + name: 'full-name', + when: (n) => ['name', 'fullname', 'displayname'].includes(normalize(n)), use: (faker) => faker.person.fullName(), }, { diff --git a/packages/generate/src/generate.test.ts b/packages/generate/src/generate.test.ts index 4792793..74ccc9a 100644 --- a/packages/generate/src/generate.test.ts +++ b/packages/generate/src/generate.test.ts @@ -144,6 +144,27 @@ describe('generate', () => { expect(typeof value.orderId).toBe('number') }) + it('gives firstName, lastName and fullName each the matching kind of name', async () => { + // Plan 6 audit, finding #6: a pasted `User` used to come out as + // `{ firstName: "Claudine Kuhn", lastName: "Edwin Bode" }`. + const shape: Shape = { + kind: 'object', + fields: [ + { name: 'firstName', shape: primitive('string'), optional: false }, + { name: 'lastName', shape: primitive('string'), optional: false }, + { name: 'fullName', shape: primitive('string'), optional: false }, + { name: 'name', shape: primitive('string'), optional: false }, + ], + } + for (const seed of [1, 7, 42, 1234]) { + const value = (await generate(shape, { seed })) as Record + expect(value.firstName).not.toMatch(/\s/) + expect(value.lastName).not.toMatch(/\s/) + expect(value.fullName).toMatch(/\S\s+\S/) + expect(value.name).toMatch(/\S\s+\S/) + } + }) + it('arrayLength is clamped to 1..1000', async () => { const shape = { kind: 'array' as const, items: primitive('integer') } expect((await generate(shape, { seed: 1, arrayLength: 0 })) as unknown[]).toHaveLength(1) @@ -305,10 +326,22 @@ const CLASSIFICATION: { rejects: ['name', 'title'], }, { - rule: 'person', + rule: 'first-name', + type: 'string', + accepts: ['firstName', 'first_name', 'givenName'], + rejects: ['name', 'lastName', 'filename', 'username'], + }, + { + rule: 'last-name', + type: 'string', + accepts: ['lastName', 'last_name', 'surname', 'familyName'], + rejects: ['name', 'firstName', 'filename', 'username'], + }, + { + rule: 'full-name', type: 'string', - accepts: ['name', 'firstName', 'displayName', 'full_name'], - rejects: ['filename', 'username', 'nameless', 'renamed'], + accepts: ['name', 'displayName', 'full_name'], + rejects: ['firstName', 'lastName', 'filename', 'username', 'nameless', 'renamed'], }, { rule: 'phone',