Repository: dubzzz/fast-check
Pull request #1890 (2021-05-21 19:36:26)

🚚 Move `record` arbitrary into /arbitrary
<!-- 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:_**

- [ ] ⚡️ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [x] _Other(s):_ Moving code
<!-- Don't forget to add the gitmoji icon in the name of the PR -->
<!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding feastures... 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/1890/builds/132478) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 6a7aef7350fa0b97168a2dbe43806002d8562eed:

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

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1890?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#1890](https://codecov.io/gh/dubzzz/fast-check/pull/1890?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (6a7aef7) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/25503945712562dade74d2c0c177a114a4c524a7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (2550394) will **increase** coverage by `0.00%`.
> The diff coverage is `96.66%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1890/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/1890?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    #1890   +/-   ##
=======================================
  Coverage   97.49%   97.49%           
=======================================
  Files         163      165    +2     
  Lines        4064     4067    +3     
  Branches      781      781           
=======================================
+ Hits         3962     3965    +3     
  Misses         98       98           
  Partials        4        4           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.49% <96.66%> (+<0.01%)` | :arrow_up: |
| unit-tests-10.x-Linux | `?` | |
| unit-tests-12.x-Linux | `97.49% <96.66%> (+<0.01%)` | :arrow_up: |
| unit-tests-14.x-Linux | `?` | |
| unit-tests-16.x-Linux | `?` | |

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/1890?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [src/arbitrary/record.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1890/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9yZWNvcmQudHM=) | `97.14% <87.50%> (ø)` | |
| [.../\_internals/builders/FullRecordArbitraryBuilder.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1890/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2J1aWxkZXJzL0Z1bGxSZWNvcmRBcmJpdHJhcnlCdWlsZGVyLnRz) | `100.00% <100.00%> (ø)` | |
| [...rary/\_internals/helpers/EnumerableKeysExtractor.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1890/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2hlbHBlcnMvRW51bWVyYWJsZUtleXNFeHRyYWN0b3IudHM=) | `100.00% <100.00%> (ø)` | |
| [src/fast-check-default.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1890/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2Zhc3QtY2hlY2stZGVmYXVsdC50cw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1890?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/1890?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [2550394...6a7aef7](https://codecov.io/gh/dubzzz/fast-check/pull/1890?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/_internals/builders/FullRecordArbitraryBuilder.ts b/src/arbitrary/_internals/builders/FullRecordArbitraryBuilder.ts
new file mode 100644
index 00000000..07a144f2
--- /dev/null
+++ b/src/arbitrary/_internals/builders/FullRecordArbitraryBuilder.ts
@@ -0,0 +1,19 @@
+import { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
+import { tuple } from '../../tuple';
+import { EnumerableKeyOf, extractEnumerableKeys } from '../helpers/EnumerableKeysExtractor';
+
+/** @internal */
+export function buildFullRecordArbitrary<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<T> {
+  const keys = extractEnumerableKeys(recordModel);
+  const arbs: Arbitrary<T[keyof T]>[] = [];
+  for (let index = 0; index !== keys.length; ++index) {
+    arbs.push(recordModel[keys[index]]);
+  }
+  return tuple(...arbs).map((gs: T[keyof T][]): T => {
+    const obj: Partial<Record<EnumerableKeyOf<T>, any>> = {};
+    for (let idx = 0; idx !== keys.length; ++idx) {
+      obj[keys[idx]] = gs[idx];
+    }
+    return obj as T;
+  });
+}
diff --git a/src/arbitrary/_internals/helpers/EnumerableKeysExtractor.ts b/src/arbitrary/_internals/helpers/EnumerableKeysExtractor.ts
new file mode 100644
index 00000000..4afb1bc5
--- /dev/null
+++ b/src/arbitrary/_internals/helpers/EnumerableKeysExtractor.ts
@@ -0,0 +1,16 @@
+/** @internal */
+export type EnumerableKeyOf<T> = Extract<keyof T, string | symbol>;
+
+/** @internal */
+export function extractEnumerableKeys<T>(instance: T): EnumerableKeyOf<T>[] {
+  const keys = Object.keys(instance) as EnumerableKeyOf<T>[]; // Only enumerable own properties
+  const symbols = Object.getOwnPropertySymbols(instance) as EnumerableKeyOf<T>[];
+  for (let index = 0; index !== symbols.length; ++index) {
+    const symbol = symbols[index];
+    const descriptor = Object.getOwnPropertyDescriptor(instance, symbol);
+    if (descriptor && descriptor.enumerable) {
+      keys.push(symbol);
+    }
+  }
+  return keys;
+}
diff --git a/src/check/arbitrary/RecordArbitrary.ts b/src/arbitrary/record.ts
similarity index 73%
rename from src/check/arbitrary/RecordArbitrary.ts
rename to src/arbitrary/record.ts
index 92194c1a..cbeefa5d 100644
--- a/src/check/arbitrary/RecordArbitrary.ts
+++ b/src/arbitrary/record.ts
@@ -1,7 +1,7 @@
-import { Arbitrary } from './definition/Arbitrary';
-
-import { option } from '../../arbitrary/option';
-import { genericTuple } from '../../arbitrary/genericTuple';
+import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { option } from './option';
+import { extractEnumerableKeys } from './_internals/helpers/EnumerableKeysExtractor';
+import { buildFullRecordArbitrary } from './_internals/builders/FullRecordArbitraryBuilder';
 
 /**
  * Constraints to be applied on {@link record}
@@ -49,39 +49,6 @@ export type RecordValue<T, TConstraints = {}> = TConstraints extends { withDelet
   ? Partial<T> & Pick<T, TKeys & keyof T>
   : T;
 
-/** @internal */
-type RecordKey<T> = Extract<keyof T, string | symbol>;
-
-/** @internal */
-function extractAllKeys<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): RecordKey<T>[] {
-  const keys = Object.keys(recordModel) as RecordKey<T>[]; // Only enumerable own properties
-  const symbols = Object.getOwnPropertySymbols(recordModel) as RecordKey<T>[];
-  for (let index = 0; index !== symbols.length; ++index) {
-    const symbol = symbols[index];
-    const descriptor = Object.getOwnPropertyDescriptor(recordModel, symbol);
-    if (descriptor && descriptor.enumerable) {
-      keys.push(symbol);
-    }
-  }
-  return keys;
-}
-
-/** @internal */
-function rawRecord<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<T> {
-  const keys = extractAllKeys(recordModel);
-  const arbs: Arbitrary<T[keyof T]>[] = [];
-  for (let index = 0; index !== keys.length; ++index) {
-    arbs.push(recordModel[keys[index]]);
-  }
-  return genericTuple(arbs).map((gs: any[]) => {
-    const obj: Record<RecordKey<T>, any> = {} as any;
-    for (let idx = 0; idx !== keys.length; ++idx) {
-      obj[keys[idx]] = gs[idx];
-    }
-    return obj as T;
-  });
-}
-
 /**
  * For records following the `recordModel` schema
  *
@@ -121,8 +88,9 @@ function record<T>(
   constraints?: RecordConstraints<keyof T>
 ): unknown {
   if (constraints == null) {
-    return rawRecord(recordModel);
+    return buildFullRecordArbitrary(recordModel);
   }
+
   if ('withDeletedKeys' in constraints && 'requiredKeys' in constraints) {
     throw new Error(`requiredKeys and withDeletedKeys cannot be used together in fc.record`);
   }
@@ -131,12 +99,10 @@ function record<T>(
     ('requiredKeys' in constraints && constraints.requiredKeys !== undefined) ||
     ('withDeletedKeys' in constraints && !!constraints.withDeletedKeys);
   if (!requireDeletedKeys) {
-    return rawRecord(recordModel);
+    return buildFullRecordArbitrary(recordModel);
   }
 
-  const updatedRecordModel: { [K in keyof T]: Arbitrary<{ value: T[K] } | null> } = {} as any;
   const requiredKeys = ('requiredKeys' in constraints ? constraints.requiredKeys : undefined) || [];
-
   for (let idx = 0; idx !== requiredKeys.length; ++idx) {
     const descriptor = Object.getOwnPropertyDescriptor(recordModel, requiredKeys[idx]);
     if (descriptor === undefined) {
@@ -147,14 +113,15 @@ function record<T>(
     }
   }
 
-  const keys = extractAllKeys(recordModel);
+  const updatedRecordModel: { [K in keyof T]?: Arbitrary<{ value: T[K] } | null> } = {};
+  const keys = extractEnumerableKeys(recordModel);
   for (let index = 0; index !== keys.length; ++index) {
     const k = keys[index];
     const requiredArbitrary = recordModel[k].map((v) => ({ value: v }));
     if (requiredKeys.indexOf(k) !== -1) updatedRecordModel[k] = requiredArbitrary;
     else updatedRecordModel[k] = option(requiredArbitrary);
   }
-  return rawRecord(updatedRecordModel as any).map((rawObj) => {
+  return buildFullRecordArbitrary(updatedRecordModel as any).map((rawObj) => {
     const obj = rawObj as { [K in keyof T]: { value: T[keyof T] } | null };
     const nobj: { [K in keyof T]?: T[keyof T] } = {};
     for (let index = 0; index !== keys.length; ++index) {
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index 77090f82..c76cce6a 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -74,7 +74,7 @@ import {
 } from './check/arbitrary/ObjectArbitrary';
 import { oneof, OneOfValue, OneOfConstraints } from './arbitrary/oneof';
 import { option, OptionConstraints } from './arbitrary/option';
-import { record, RecordConstraints, RecordValue } from './check/arbitrary/RecordArbitrary';
+import { record, RecordConstraints, RecordValue } from './arbitrary/record';
 import { set, SetConstraints } from './arbitrary/set';
 import { infiniteStream } from './arbitrary/infiniteStream';
 import { asciiString } from './arbitrary/asciiString';


Test patch:
diff --git a/test/unit/check/arbitrary/RecordArbitrary.spec.ts b/test/unit/arbitrary/RecordArbitrary.spec.ts
similarity index 93%
rename from test/unit/check/arbitrary/RecordArbitrary.spec.ts
rename to test/unit/arbitrary/RecordArbitrary.spec.ts
index 035c7465..9b313231 100644
--- a/test/unit/check/arbitrary/RecordArbitrary.spec.ts
+++ b/test/unit/arbitrary/RecordArbitrary.spec.ts
@@ -1,13 +1,13 @@
 import * as prand from 'pure-rand';
-import * as fc from '../../../../lib/fast-check';
+import * as fc from '../../../lib/fast-check';
 
-import { constant } from '../../../../src/arbitrary/constant';
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { integer } from '../../../../src/arbitrary/integer';
-import { record, RecordConstraints } from '../../../../src/check/arbitrary/RecordArbitrary';
-import { Random } from '../../../../src/random/generator/Random';
+import { constant } from '../../../src/arbitrary/constant';
+import { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary';
+import { integer } from '../../../src/arbitrary/integer';
+import { record, RecordConstraints } from '../../../src/arbitrary/record';
+import { Random } from '../../../src/random/generator/Random';
 
-import * as genericHelper from './generic/GenericArbitraryHelper';
+import * as genericHelper from '../check/arbitrary/generic/GenericArbitraryHelper';
 
 const keyArb: fc.Arbitrary<any> = fc
   .tuple(fc.string(), fc.boolean())