Repository: dubzzz/fast-check
Pull request #2019 (2021-06-21 19:05:37)

✨ Expose custom to string methods
<!-- 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):_ ...



Patch:
diff --git a/documentation/Tips.md b/documentation/Tips.md
index f0399eec..18a67ff0 100644
--- a/documentation/Tips.md
+++ b/documentation/Tips.md
@@ -21,6 +21,7 @@ Simple tips to unlock all the power of fast-check with only few changes.
 - [Create a CodeSandbox link on error](#create-a-codesandbox-link-on-error)
 - [Migrate from jsverify to fast-check](#migrate-from-jsverify-to-fast-check)
 - [Supported targets from node to deno](#supported-targets-from-node-to-deno)
+- [Override default toString for a given instance](#override-default-toString-for-a-given-instance)
 
 ## Filter invalid combinations using pre-conditions
 
@@ -805,3 +806,37 @@ Web Browser:
 ```
 
 More details on [pika](https://www.pika.dev/npm/fast-check/).
+
+## Override default toString for a given instance
+
+By default `fast-check` will serialize the generated values using it's internal stringify helper: `fc.stringify`. From time to time you may be interested in having better stringified versions of your instances. In order to do that `fast-check` offers multiple solutions:
+
+1. If your instance defines a `toString` method then `fast-check` will use it to properly report this instance. Except if one of the following one has been defined (they have higher priorities).
+2. But defining `toString` might be to intrusive. In such case `fast-check` comes with two methods: `fc.toStringMethod` and `fc.asyncToStringMethod`.
+
+Most of the time you may only need to define `fc.toStringMethod`. `fc.toStringMethod` is the serializer method that will be used by `fast-check` to serialize your instance whatever the context: asynchronous or synchronous properties.
+
+```ts
+Object.defineProperties(
+  myInstanceWithoutCustomToString,
+  { [fc.toStringMethod]: { value: () => 'my-value' } }
+);
+// here your instance defines a custom serializer that will be used by fast-check
+// whenever needed
+```
+
+But when working with asynchronous values, you may need an async code to get back the value. For instance:
+
+```ts
+Object.defineProperties(
+  myPromisePossiblyResolved,
+  { [fc.asyncToStringMethod]: { value: async () => {
+    const resolved = await myPromisePossiblyResolved;
+    return `My value: ${resolved}`;
+  } } }
+);
+```
+
+Please note that:
+1. `fc.asyncToStringMethod` will only be used in the context of asynchronous properties
+2. While `fc.asyncToStringMethod` is marked as asynchronous it should not be too long. More precisely it should resolve barely instantly.
diff --git a/src/check/symbols.ts b/src/check/symbols.ts
index 08c342e6..3ce524e1 100644
--- a/src/check/symbols.ts
+++ b/src/check/symbols.ts
@@ -32,7 +32,12 @@ export function hasCloneMethod<T>(instance: T | WithCloneMethod<T>): instance is
   //   [], {}, () => {}, function() {}, async () => {}, async function() {}
   // Invalid ones:
   //   1, "", Symbol(), null, undefined
-  return instance instanceof Object && typeof (instance as any)[cloneMethod] === 'function';
+  return (
+    instance !== null &&
+    (typeof instance === 'object' || typeof instance === 'function') &&
+    cloneMethod in instance &&
+    typeof (instance as any)[cloneMethod] === 'function'
+  );
 }
 
 /**
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index 72576435..e1450a93 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -127,7 +127,16 @@ import { ExecutionTree } from './check/runner/reporter/ExecutionTree';
 import { cloneMethod, cloneIfNeeded, hasCloneMethod, WithCloneMethod } from './check/symbols';
 import { Stream, stream } from './stream/Stream';
 import { hash } from './utils/hash';
-import { stringify, asyncStringify } from './utils/stringify';
+import {
+  stringify,
+  asyncStringify,
+  toStringMethod,
+  hasToStringMethod,
+  WithToStringMethod,
+  asyncToStringMethod,
+  hasAsyncToStringMethod,
+  WithAsyncToStringMethod,
+} from './utils/stringify';
 import {
   scheduler,
   schedulerFor,
@@ -338,6 +347,12 @@ export {
   convertFromNext,
   convertFromNextWithShrunkOnce,
   convertToNext,
+  toStringMethod,
+  hasToStringMethod,
+  WithToStringMethod,
+  asyncToStringMethod,
+  hasAsyncToStringMethod,
+  WithAsyncToStringMethod,
   // print values
   stringify,
   asyncStringify,
diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts
index 2253b06c..69a9525b 100644
--- a/src/utils/stringify.ts
+++ b/src/utils/stringify.ts
@@ -1,11 +1,24 @@
 /**
- * Ability to override the default output of stringified for a given value
- * @internal
+ * Use this symbol to define a custom serializer for your instances.
+ * Serializer must be a function returning a string (see {@link WithToStringMethod}).
+ *
+ * @remarks Since 2.17.0
+ * @public
  */
 export const toStringMethod = Symbol('fast-check/toStringMethod');
-/** @internal */
+/**
+ * Interface to implement for {@link toStringMethod}
+ *
+ * @remarks Since 2.17.0
+ * @public
+ */
 export type WithToStringMethod = { [toStringMethod]: () => string };
-/** @internal */
+/**
+ * Check if an instance implements {@link WithToStringMethod}
+ *
+ * @remarks Since 2.17.0
+ * @public
+ */
 export function hasToStringMethod<T>(instance: T): instance is T & WithToStringMethod {
   return (
     instance !== null &&
@@ -16,13 +29,30 @@ export function hasToStringMethod<T>(instance: T): instance is T & WithToStringM
 }
 
 /**
- * Ability to override the default output of stringified for a given value
- * @internal
+ * Use this symbol to define a custom serializer for your instances.
+ * Serializer must be a function returning a promise of string (see {@link WithAsyncToStringMethod}).
+ *
+ * Please note that:
+ * 1. It will only be useful for asynchronous properties.
+ * 2. It has to return barely instantly.
+ *
+ * @remarks Since 2.17.0
+ * @public
  */
 export const asyncToStringMethod = Symbol('fast-check/asyncToStringMethod');
-/** @internal */
+/**
+ * Interface to implement for {@link asyncToStringMethod}
+ *
+ * @remarks Since 2.17.0
+ * @public
+ */
 export type WithAsyncToStringMethod = { [asyncToStringMethod]: () => Promise<string> };
-/** @internal */
+/**
+ * Check if an instance implements {@link WithAsyncToStringMethod}
+ *
+ * @remarks Since 2.17.0
+ * @public
+ */
 export function hasAsyncToStringMethod<T>(instance: T): instance is T & WithAsyncToStringMethod {
   return (
     instance !== null &&


Test patch:
diff --git a/test/unit/check/symbols.spec.ts b/test/unit/check/symbols.spec.ts
index c7149393..7950377a 100644
--- a/test/unit/check/symbols.spec.ts
+++ b/test/unit/check/symbols.spec.ts
@@ -1,4 +1,4 @@
-import { cloneMethod } from '../../../src/check/symbols';
+import { cloneMethod, hasCloneMethod } from '../../../src/check/symbols';
 import * as fc from '../../../lib/fast-check';
 
 describe('symbols', () => {
@@ -6,3 +6,29 @@ describe('symbols', () => {
     expect(cloneMethod).not.toBe(fc.cloneMethod);
   });
 });
+
+describe('hasCloneMethod', () => {
+  it.each`
+    obj                    | description
+    ${{}}                  | ${'simple objects'}
+    ${Object.create(null)} | ${'prototype-less objects'}
+    ${[]}                  | ${'arrays'}
+    ${() => {}}            | ${'functions'}
+  `('should be able to detect [cloneMethod] on $description', ({ obj }) => {
+    Object.defineProperty(obj, cloneMethod, { value: () => 'yes' });
+    expect(hasCloneMethod(obj)).toBe(true);
+  });
+
+  it.each`
+    obj                    | description
+    ${1}                   | ${'number [primitive]'}
+    ${'1'}                 | ${'string [primitive]'}
+    ${Symbol()}            | ${'symbol [primitive]'}
+    ${{}}                  | ${'simple objects'}
+    ${Object.create(null)} | ${'prototype-less objects'}
+    ${[]}                  | ${'arrays'}
+    ${() => {}}            | ${'functions'}
+  `('should not be able to detect [cloneMethod] when not provided on $description', ({ obj }) => {
+    expect(hasCloneMethod(obj)).toBe(false);
+  });
+});
diff --git a/test/unit/utils/stringify.spec.ts b/test/unit/utils/stringify.spec.ts
index 9dda59e2..80739955 100644
--- a/test/unit/utils/stringify.spec.ts
+++ b/test/unit/utils/stringify.spec.ts
@@ -566,6 +566,10 @@ describe('asyncStringify', () => {
       },
     };
     expect(await asyncStringify(instance9)).toEqual('Promise.resolve("hello9") Promise.resolve("world9")');
+
+    const p10 = Promise.resolve('hello10');
+    const instance10 = { [asyncToStringMethod]: () => p10.then((v) => `got: ${v}`) };
+    expect(await asyncStringify(instance10)).toEqual('got: hello10');
   });
 });