Skip to content

Commit

Permalink
feat: Initial implementation of limit option (#30)
Browse files Browse the repository at this point in the history
## Video with context
https://www.loom.com/share/b94094839ca04524ab661b8837eebf6e

## Problem
At the moment, copycat gives you the ability to generate some fake value corresponding to some input value, for example:

```js
copycat.email('real@email.org')
// => 'Era_Kunde245@gmail.com'
```

However, there is currently no way to restrict the generated values to be within a given character limit.

At [snaplet](https://www.snaplet.dev/), we make use of copycat to replace real values in a database with fake values.

This is where the problem comes in: the real values in the database are within some character limit. However, the fake values generated by copycat that we're replacing them with are not always within this same character limit. As a result, we aren't able to use these fake values.

## Solution
Add a `limit` option to each copycat API method that generates a string, for example:

```
// generated result will be <= 20 characters
copycat.email('real@example.org', { limit: 20 })
```

## Approach
Copycat makes use of composition to generate values. For example, `copycat.email()` makes use of `copycat.firstName()` and `copycat.lastName()`.

The idea is to have each component in each layer of composition be aware of the character limit. For composites like `email`, allocate a `limit` to each component. For example, if the limit is `25`, then give `5` as a limit for `firstName`.

Then, for each primitive/leaf (the end of the chain of composition), have it restrict its output to be within that limit. For example, `firstName` makes use of `oneOf()`, and uses it to pick from an array of first names (provided by faker). So this PR replaces this `oneOf()` usage with a new `oneOfString()` function, which will only pick from the list of values that are within that limit. If none are found, it defaults to using `copycat.word()`.

For more context on the approach, take a look at the video: https://www.loom.com/share/b94094839ca04524ab661b8837eebf6e
  • Loading branch information
justinvdm committed Oct 11, 2022
1 parent f09303e commit 8babbfa
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 27 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ copycat.email('foo')
// => 'Zakary.Block356@gmail.com'
```

#### `options`

- **`limit`:** Constrain generated values to be less than or equal to `limit` number of chars

### `copycat.firstName(input)`

Takes in an [input](#input) and returns a string value resembling a first name.
Expand All @@ -256,6 +260,10 @@ copycat.firstName('foo')
// => 'Alejandrin'
```

#### `options`

- **`limit`:** Constrain generated values to be less than or equal to `limit` number of chars

### `copycat.lastName(input)`

Takes in an [input](#input) and returns a string value resembling a last name.
Expand All @@ -265,6 +273,10 @@ copycat.lastName('foo')
// => 'Keeling'
```

#### `options`

- **`limit`:** Constrain generated values to be less than or equal to `limit` number of chars

### `copycat.fullName(input)`

Takes in an [input](#input) and returns a string value resembling a full name.
Expand All @@ -274,6 +286,10 @@ copycat.fullName('foo')
// => 'Zakary Hessel'
```

#### `options`

- **`limit`:** Constrain generated values to be less than or equal to `limit` number of chars

### `copycat.phoneNumber(input)`

Takes in an [input](#input) and returns a string value resembling a [phone number](https://en.wikipedia.org/wiki/MSISDN).
Expand Down
249 changes: 249 additions & 0 deletions src/copycat.limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { copycat } from '.'

const NUM_CHECKS = 10

export const LIMIT_TRANSFORMATION_NAMES = [
'email',
'firstName',
'lastName',
'fullName',
] as const

const generateValues = (limit: number) => {
const results = {}

for (const name of LIMIT_TRANSFORMATION_NAMES) {
let i = -1
const fn = copycat[name]
const transformationResults: unknown[] = []
results[name] = transformationResults

while (++i < NUM_CHECKS) {
const result = fn(i, { limit })
expect(result.length).toBeLessThanOrEqual(limit)
transformationResults.push(result)
}
}

return results
}

test('limit: medium', () => {
expect(generateValues(25)).toMatchInlineSnapshot(`
Object {
"email": Array [
"Bo_King114@gmail.net",
"Bo_Koch349@yahoo.net",
"Bo_Ward471@yahoo.com",
"Bo_Toy949@gmail.com",
"Ed_Dare472@yahoo.com",
"Bo_Wiza59@yahoo.info",
"Bo_Toy189@gmail.info",
"Bo_Von60@gmail.biz",
"Ed_Kub941@gmail.org",
"Ed_Cole773@yahoo.net",
],
"firstName": Array [
"Cindy",
"Amara",
"Zelma",
"Glennie",
"Kaley",
"Betty",
"Laurianne",
"Horace",
"Wilson",
"Kamryn",
],
"fullName": Array [
"Myrl Heidenreich",
"Ignacio Reinger",
"Vesta Smith",
"Ottis Stark",
"Nolan Rutherford",
"Ernesto Jacobs",
"Eleanora Boyle",
"Jaiden Muller",
"Willow Osinski",
"Jane Glover",
],
"lastName": Array [
"Nitzsche",
"Ledner",
"Jakubowski",
"Boyle",
"Emard",
"Breitenberg",
"Yundt",
"Davis",
"Zulauf",
"Kuphal",
],
}
`)
})

test('limit: small', () => {
expect(generateValues(10)).toMatchInlineSnapshot(`
Object {
"email": Array [
"VvK1@ko.yu",
"MmK3@mi.ra",
"SsK4@vi.vi",
"KsY9@mi.so",
"MmR4@yu.ko",
"YrK5@yu.vi",
"KkS1@ra.vi",
"MmR6@ko.ra",
"YsM9@yu.yu",
"MyV7@so.yu",
],
"firstName": Array [
"Eryn",
"Osborne",
"Lamar",
"Lance",
"Frank",
"Breanna",
"Alden",
"Stewart",
"Rebeka",
"Kira",
],
"fullName": Array [
"Bud Yost",
"Ena Batz",
"Ian Koch",
"Tom Ward",
"Tre Haag",
"Roy Rowe",
"Loy Conn",
"Ima Ward",
"Guy Lowe",
"Rae Fay",
],
"lastName": Array [
"Abernathy",
"Kris",
"Wyman",
"Kessler",
"Braun",
"Mante",
"Hirthe",
"Abbott",
"Gerlach",
"Dibbert",
],
}
`)
})

test('limit: very large', () => {
expect(generateValues(999)).toMatchInlineSnapshot(`
Object {
"email": Array [
"Liliane_Powlowski114@gmail.net",
"Emely_Buckridge349@yahoo.net",
"Jeffry_Kshlerin471@yahoo.com",
"Norbert_Funk949@gmail.com",
"Lyda_Schowalter472@yahoo.com",
"Kaylie_Yost59@yahoo.info",
"Catherine_Schmitt189@gmail.info",
"Elinore_Kshlerin60@gmail.biz",
"Jace_Boehm941@gmail.org",
"Howell_Bergnaum773@yahoo.net",
],
"firstName": Array [
"Cindy",
"Amara",
"Zelma",
"Glennie",
"Kaley",
"Betty",
"Laurianne",
"Horace",
"Wilson",
"Kamryn",
],
"fullName": Array [
"Liliane Heidenreich",
"Emely Reinger",
"Jeffry Smith",
"Norbert Stark",
"Lyda Rutherford",
"Kaylie Jacobs",
"Catherine Boyle",
"Elinore Muller",
"Jace Osinski",
"Howell Glover",
],
"lastName": Array [
"Nitzsche",
"Ledner",
"Jakubowski",
"Boyle",
"Emard",
"Breitenberg",
"Yundt",
"Davis",
"Zulauf",
"Kuphal",
],
}
`)
})

test('limit: tiny', () => {
expect(generateValues(5)).toMatchInlineSnapshot(`
Object {
"email": Array [
"1@k.y",
"3@m.r",
"4@v.v",
"9@m.s",
"4@y.k",
"5@y.v",
"1@r.v",
"6@k.r",
"9@y.y",
"7@s.y",
],
"firstName": Array [
"Otto",
"Nils",
"Bria",
"Jo",
"Alec",
"Kaci",
"Adah",
"Clay",
"Nick",
"Isac",
],
"fullName": Array [
"V Vi",
"M Mi",
"S So",
"K So",
"M Mi",
"Y Ra",
"K Ko",
"M Mi",
"Y So",
"M Yu",
],
"lastName": Array [
"Dare",
"Rau",
"Howe",
"Kris",
"King",
"Cole",
"Koss",
"Howe",
"Roob",
"Roob",
],
}
`)
})
38 changes: 25 additions & 13 deletions src/email.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import faker from '@faker-js/faker'
import { int, oneOf, join } from 'fictional'
import { int } from 'fictional'
import { firstName } from './firstName'
import { join } from './join'
import { lastName } from './lastName'
import { oneOfString } from './oneOfString'

import { Input } from './types'

const maker = join('', [
firstName,
oneOf(['_', '.']),
lastName,
int.options({
min: 2,
max: 999,
}),
'@',
oneOf(faker.locales.en!.internet!.free_email!),
])
interface EmailOptions {
limit?: number
}

export const email = (input: Input): string => maker(input)
export const email = (input: Input, options: EmailOptions = {}): string =>
join(
input,
'',
[
firstName,
oneOfString(['_', '.']),
lastName,
int.options({
min: 2,
max: 999,
}),
'@',
oneOfString(['gmail', 'yahoo', 'hotmail']),
'.',
oneOfString(faker.locales.en!.internet!.domain_suffix!),
],
options
)
11 changes: 6 additions & 5 deletions src/firstName.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import faker from '@faker-js/faker'
import { oneOf } from 'fictional'
import { oneOfString } from './oneOfString'
import { word } from './primitives'

import { Input } from './types'

export const firstName = (input: Input): string =>
oneOf(input, faker.locales.en!.name!.first_name!)
export const firstName = oneOfString(
faker.locales.en!.name!.first_name!,
word.options({ capitalize: true })
)
7 changes: 3 additions & 4 deletions src/fullName.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { join } from 'fictional'

import { Input } from './types'
import { firstName } from './firstName'
import { lastName } from './lastName'
import { join } from './join'

export const fullName = (input: Input): string =>
join(input, ' ', [firstName, lastName])
export const fullName = (input: Input, options = {}): string =>
join(input, ' ', [firstName, lastName], options)
Loading

0 comments on commit 8babbfa

Please sign in to comment.