Repository: dubzzz/fast-check
Pull request #2036 (2021-06-24 22:43:31)

✨ Add built-in unmapper support onto `emailAddress`
<!-- Context of the PR: short description and potentially linked issues -->

<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->
**_Category:_**

- [x] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [ ] _Other(s):_ ...
<!-- Don't forget to add the gitmoji icon in the name of the PR -->
<!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding features... may impact existing ones           -->
<!-- in order to track potential issues that could be related to your PR -->
<!-- please check the impacts and describe more precisely what to expect -->

**_Potential impacts:_**

<!-- Generated values: Can your change impact any of the existing generators in terms of generated values, if so which ones? when? -->
<!-- Shrink values:    Can your change impact any of the existing generators in terms of shrink values, if so which ones? when? -->
<!-- Performance:      Can it require some typings changes on user side? Please give more details -->
<!-- Typings:          Is there a potential performance impact? In which cases? -->
- [ ] Generated values
- [ ] Shrink values
- [ ] Performance
- [ ] Typings
- [ ] _Other(s):_ ...


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/2036/builds/143426) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 464c8243cce2a4d37e269c2104c0c970e4f7a347:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-3uyc0)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-i0f0h)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#2036](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (464c824) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/f47f28d0169d1b2084f1677141221f0cbfb96976?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (f47f28d) will **decrease** coverage by `0.03%`.
> The diff coverage is `87.50%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/2036/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)

```diff
@@            Coverage Diff             @@
##             main    #2036      +/-   ##
==========================================
- Coverage   96.54%   96.51%   -0.04%     
==========================================
  Files         176      176              
  Lines        4346     4358      +12     
  Branches      810      812       +2     
==========================================
+ Hits         4196     4206      +10     
- Misses        146      148       +2     
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `96.51% <87.50%> (-0.04%)` | :arrow_down: |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `?` | |
| unit-tests-16.x-Linux | `96.51% <87.50%> (-0.04%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [src/arbitrary/emailAddress.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2036/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9lbWFpbEFkZHJlc3MudHM=) | `91.66% <87.50%> (-8.34%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [f47f28d...464c824](https://codecov.io/gh/dubzzz/fast-check/pull/2036?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).


Patch:
diff --git a/src/arbitrary/emailAddress.ts b/src/arbitrary/emailAddress.ts
index 1eda800f..457b034c 100644
--- a/src/arbitrary/emailAddress.ts
+++ b/src/arbitrary/emailAddress.ts
@@ -4,6 +4,30 @@ import { domain } from './domain';
 import { stringOf } from './stringOf';
 import { tuple } from './tuple';
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
+
+/** @internal */
+function dotMapper(a: string[]): string {
+  return a.join('.');
+}
+/** @internal */
+function dotUnmapper(value: unknown): string[] {
+  if (typeof value !== 'string') {
+    throw new Error('Unsupported');
+  }
+  return value.split('.');
+}
+/** @internal */
+function atMapper(data: [string, string]): string {
+  return `${data[0]}@${data[1]}`;
+}
+/** @internal */
+function atUnmapper(value: unknown): [string, string] {
+  if (typeof value !== 'string') {
+    throw new Error('Unsupported');
+  }
+  return value.split('@', 2) as [string, string];
+}
 
 /**
  * For email address
@@ -18,11 +42,13 @@ import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
 export function emailAddress(): Arbitrary<string> {
   const others = ['!', '#', '$', '%', '&', "'", '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~'];
   const atextArb = buildLowerAlphaNumericArbitrary(others);
-  const localPartArb = array(stringOf(atextArb, { minLength: 1, maxLength: 10 }), { minLength: 1, maxLength: 5 })
-    .map((a) => a.join('.'))
-    // According to RFC 2821:
-    //    The maximum total length of a user name or other local-part is 64 characters.
-    .filter((lp) => lp.length <= 64);
+  const localPartArb = convertFromNext(
+    convertToNext(array(stringOf(atextArb, { minLength: 1, maxLength: 10 }), { minLength: 1, maxLength: 5 }))
+      .map(dotMapper, dotUnmapper)
+      // According to RFC 2821:
+      //    The maximum total length of a user name or other local-part is 64 characters.
+      .filter((lp) => lp.length <= 64)
+  );
 
-  return tuple(localPartArb, domain()).map(([lp, d]) => `${lp}@${d}`);
+  return convertFromNext(convertToNext(tuple(localPartArb, domain())).map(atMapper, atUnmapper));
 }


Test patch:
diff --git a/test/unit/arbitrary/EmailArbitrary.spec.ts b/test/unit/arbitrary/EmailArbitrary.spec.ts
deleted file mode 100644
index 3e6486b8..00000000
--- a/test/unit/arbitrary/EmailArbitrary.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { emailAddress } from '../../../src/arbitrary/emailAddress';
-
-import * as genericHelper from '../check/arbitrary/generic/GenericArbitraryHelper';
-
-const isValidEmailRfc1123 = (t: string) => {
-  // Taken from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
-  const rfc1123 =
-    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
-  return rfc1123.test(t);
-};
-
-const isValidEmailRfc2821 = (t: string) => {
-  const [localPart, domain] = t.split('@');
-  // The maximum total length of a user name or other local-part is 64 characters.
-  // The maximum total length of a domain name or number is 255 characters.
-  return localPart.length <= 64 && domain.length <= 255;
-};
-
-const isValidEmailRfc5322 = (t: string) => {
-  // Taken from https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
-  const rfc5322 =
-    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-  return rfc5322.test(t);
-};
-
-describe('EmailArbitrary', () => {
-  describe('emailAddress', () => {
-    describe('RFC 1123', () => {
-      genericHelper.isValidArbitrary(() => emailAddress(), {
-        isValidValue: (g: string) => isValidEmailRfc1123(g),
-      });
-    });
-    describe('RFC 2821', () => {
-      genericHelper.isValidArbitrary(() => emailAddress(), {
-        isValidValue: (g: string) => isValidEmailRfc2821(g),
-      });
-    });
-    describe('RFC 5322', () => {
-      genericHelper.isValidArbitrary(() => emailAddress(), {
-        isValidValue: (g: string) => isValidEmailRfc5322(g),
-      });
-    });
-  });
-});
diff --git a/test/unit/arbitrary/__snapshots__/emailAddress.spec.ts.snap b/test/unit/arbitrary/__snapshots__/emailAddress.spec.ts.snap
new file mode 100644
index 00000000..552101e9
--- /dev/null
+++ b/test/unit/arbitrary/__snapshots__/emailAddress.spec.ts.snap
@@ -0,0 +1,112 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`emailAddress (integration) should be able to shrink me@domain.com 1`] = `
+"\\"me@domain.com\\"
+├> \\"e@domain.com\\"
+|  ├> \\"a@domain.com\\"
+|  |  ├> \\"a@aomain.com\\"
+|  |  |  ├> \\"a@a.com\\"
+|  |  |  |  ├> \\"a@a.om\\"
+|  |  |  |  |  ├> \\"a@a.am\\"
+|  |  |  |  |  ├> \\"a@a.hm\\"
+|  |  |  |  |  |  ├> \\"a@a.em\\"
+|  |  |  |  |  |  |  ├> \\"a@a.cm\\"
+|  |  |  |  |  |  |  |  └> \\"a@a.bm\\"
+|  |  |  |  |  |  |  |     └> \\"a@a.am\\"
+|  |  |  |  |  |  |  └> \\"a@a.dm\\"
+|  |  |  |  |  |  |     └> \\"a@a.cm\\"
+|  |  |  |  |  |  |        ├> \\"a@a.am\\"
+|  |  |  |  |  |  |        └> \\"a@a.bm\\"
+|  |  |  |  |  |  |           └> \\"a@a.am\\"
+|  |  |  |  |  |  └> \\"a@a.gm\\"
+|  |  |  |  |  |     └> \\"a@a.fm\\"
+|  |  |  |  |  |        └> \\"a@a.em\\"
+|  |  |  |  |  |           ├> \\"a@a.am\\"
+|  |  |  |  |  |           ├> \\"a@a.cm\\"
+|  |  |  |  |  |           |  └> \\"a@a.bm\\"
+|  |  |  |  |  |           |     └> \\"a@a.am\\"
+|  |  |  |  |  |           └> \\"a@a.dm\\"
+|  |  |  |  |  |              └> \\"a@a.cm\\"
+|  |  |  |  |  |                 ├> \\"a@a.am\\"
+|  |  |  |  |  |                 └> \\"a@a.bm\\"
+|  |  |  |  |  |                    └> \\"a@a.am\\"
+|  |  |  |  |  ├> \\"a@a.lm\\"
+|  |  |  |  |  |  ├> \\"a@a.jm\\"
+|  |  |  |  |  |  |  └> \\"a@a.im\\"
+|  |  |  |  |  |  |     └> \\"a@a.hm\\"
+|  |  |  |  |  |  |        ├> \\"a@a.am\\"
+|  |  |  |  |  |  |        ├> \\"a@a.em\\"
+|  |  |  |  |  |  |        |  ├> \\"a@a.cm\\"
+|  |  |  |  |  |  |        |  |  └> \\"a@a.bm\\"
+|  |  |  |  |  |  |        |  |     └> \\"a@a.am\\"
+|  |  |  |  |  |  |        |  └> \\"a@a.dm\\"
+|  |  |  |  |  |  |        |     └> \\"a@a.cm\\"
+|  |  |  |  |  |  |        |        ├> \\"a@a.am\\"
+|  |  |  |  |  |  |        |        └> \\"a@a.bm\\"
+|  |  |  |  |  |  |        |           └> \\"a@a.am\\"
+|  |  |  |  |  |  |        └> \\"a@a.gm\\"
+|  |  |  |  |  |  |           └> \\"a@a.fm\\"
+|  |  |  |  |  |  |              └> \\"a@a.em\\"
+|  |  |  |  |  |  |                 ├> \\"a@a.am\\"
+|  |  |  |  |  |  |                 ├> \\"a@a.cm\\"
+|  |  |  |  |  |  |                 |  └> \\"a@a.bm\\"
+|  |  |  |  |  |  |                 |     └> \\"a@a.am\\"
+|  |  |  |  |  |  |                 └> \\"a@a.dm\\"
+|  |  |  |  |  |  |                    └> \\"a@a.cm\\"
+|  |  |  |  |  |  |                       ├> \\"a@a.am\\"
+|  |  |  |  |  |  |                       └> \\"a@a.bm\\"
+|  |  |  |  |  |  |                          └> \\"a@a.am\\"
+|  |  |  |  |  |  └> \\"a@a.km\\"
+|  |  |  |  |  |     └> \\"a@a.jm\\"
+|  |  |  |  |  |        ├> \\"a@a.am\\"
+|  |  |  |  |  |        ├> \\"a@a.fm\\"
+|  |  |  |  |  |        |  ├> \\"a@a.dm\\"
+|  |  |  |  |  |        |  |  └> \\"a@a.cm\\"
+|  |  |  |  |  |        |  |     └> \\"a@a.bm\\"
+|  |  |  |  |  |        |  |        └> \\"a@a.am\\"
+|  |  |  |  |  |        |  └> \\"a@a.em\\"
+|  |  |  |  |  |        |     └> \\"a@a.dm\\"
+|  |  |  |  |  |        |        ├> \\"a@a.am\\"
+|  |  |  |  |  |        |        └> \\"a@a.cm\\"
+|  |  |  |  |  |        |           └> \\"a@a.bm\\"
+|  |  |  |  |  |        |              └> \\"a@a.am\\"
+|  |  |  |  |  |        ├> \\"a@a.hm\\"
+|  |  |  |  |  |        |  └> \\"a@a.gm\\"
+|  |  |  |  |  |        |     └> \\"a@a.fm\\"
+|  |  |  |  |  |        |        ├> \\"a@a.am\\"
+|  |  |  |  |  |        |        ├> \\"a@a.dm\\"
+|  |  |  |  |  |        |        |  └> \\"a@a.cm\\"
+|  |  |  |  |  |        |        |     └> \\"a@a.bm\\"
+|  |  |  |  |  |        |        |        └> \\"a@a.am\\"
+|  |  |  |  |  |        |        └> \\"a@a.em\\"
+|  |  |  |  |  |        |           └> \\"a@a.dm\\"
+|  |  |  |  |  |        |              ├> \\"a@a.am\\"
+|  |  |  |  |  |        |              └> \\"a@a.cm\\"
+|  |  |  |  |  |        |                 └> \\"a@a.bm\\"
+|  |  |  |  |  |        |                    └> \\"a@a.am\\"
+|  |  |  |  |  |        └> \\"a@a.im\\"
+|  |  |  |  |  |           └> \\"a@a.hm\\"
+|  |  |  |  |  |              ├> \\"a@a.am\\"
+|  |  |  |  |  |              ├> \\"a@a.em\\"
+|  |  |  |  |  |              |  ├> \\"a@a.cm\\"
+|  |  |  |  |  |              |  |  └> \\"a@a.bm\\"
+|  |  |  |  |  |              |  |     └> \\"a@a.am\\"
+|  |  |  |  |  |              |  └> \\"a@a.dm\\"
+|  |  |  |  |  |              |     └> \\"a@a.cm\\"
+|  |  |  |  |  |              |        ├> \\"a@a.am\\"
+|  |  |  |  |  |              |        └> \\"a@a.bm\\"
+|  |  |  |  |  |              |           └> \\"a@a.am\\"
+|  |  |  |  |  |              └> \\"a@a.gm\\"
+|  |  |  |  |  |                 └> \\"a@a.fm\\"
+|  |  |  |  |  |                    └> \\"a@a.em\\"
+|  |  |  |  |  |                       ├> \\"a@a.am\\"
+|  |  |  |  |  |                       ├> \\"a@a.cm\\"
+|  |  |  |  |  |                       |  └> …
+|  |  |  |  |  |                       └> …
+|  |  |  |  |  └> …
+|  |  |  |  └> …
+|  |  |  └> …
+|  |  └> …
+|  └> …
+└> …"
+`;
diff --git a/test/unit/arbitrary/emailAddress.spec.ts b/test/unit/arbitrary/emailAddress.spec.ts
new file mode 100644
index 00000000..012fd0d0
--- /dev/null
+++ b/test/unit/arbitrary/emailAddress.spec.ts
@@ -0,0 +1,83 @@
+import fc from '../../../lib/fast-check';
+import { emailAddress } from '../../../src/arbitrary/emailAddress';
+import { convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+import { NextValue } from '../../../src/check/arbitrary/definition/NextValue';
+
+import {
+  assertProduceSameValueGivenSameSeed,
+  assertProduceCorrectValues,
+  assertProduceValuesShrinkableWithoutContext,
+  assertShrinkProducesSameValueWithoutInitialContext,
+} from '../check/arbitrary/generic/NextArbitraryAssertions';
+import { buildNextShrinkTree, renderTree } from '../check/arbitrary/generic/ShrinkTree';
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('emailAddress (integration)', () => {
+  const expectValidEmailRfc1123 = (t: string) => {
+    // Taken from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
+    const rfc1123 =
+      /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+    expect(t).toMatch(rfc1123);
+  };
+
+  const expectValidEmailRfc2821 = (t: string) => {
+    const [localPart, domain] = t.split('@');
+    // The maximum total length of a user name or other local-part is 64 characters.
+    expect(localPart.length).toBeLessThanOrEqual(64);
+    // The maximum total length of a domain name or number is 255 characters.
+    expect(domain.length).toBeLessThanOrEqual(255);
+  };
+
+  const expectValidEmailRfc5322 = (t: string) => {
+    // Taken from https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
+    const rfc5322 =
+      /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+    expect(t).toMatch(rfc5322);
+  };
+
+  const isCorrect = (t: string) => {
+    expectValidEmailRfc1123(t);
+    expectValidEmailRfc2821(t);
+    expectValidEmailRfc5322(t);
+  };
+
+  const emailAddressBuilder = () => convertToNext(emailAddress());
+
+  it('should produce the same values given the same seed', () => {
+    assertProduceSameValueGivenSameSeed(emailAddressBuilder);
+  });
+
+  it('should only produce correct values', () => {
+    assertProduceCorrectValues(emailAddressBuilder, isCorrect);
+  });
+
+  it('should produce values seen as shrinkable without any context', () => {
+    assertProduceValuesShrinkableWithoutContext(emailAddressBuilder);
+  });
+
+  it('should be able to shrink to the same values without initial context', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(emailAddressBuilder);
+  });
+
+  it.each`
+    rawValue
+    ${'me@domain.com'}
+  `('should be able to shrink $rawValue', ({ rawValue }) => {
+    // Arrange
+    const arb = convertToNext(emailAddress());
+    const value = new NextValue(rawValue, undefined);
+
+    // Act
+    const renderedTree = renderTree(buildNextShrinkTree(arb, value, { numItems: 100 })).join('\n');
+
+    // Assert
+    expect(arb.canShrinkWithoutContext(rawValue)).toBe(true);
+    expect(renderedTree).toMatchSnapshot();
+  });
+});