From 4154d8a4d7ea008e686c31942da499d4178a21d9 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Tue, 30 Jul 2024 17:14:08 +0700 Subject: [PATCH] Cover some edge cases --- apps/website/docs/.vitepress/config.mjs | 4 ++ .../docs/contracts/cookbook/merge_objects.md | 24 ++++++++++++ packages/contracts/src/contract.test-d.ts | 10 ++++- packages/contracts/src/contracts.test.ts | 24 ++++++++++++ packages/contracts/src/index.ts | 38 +++++++++++++++++++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 apps/website/docs/contracts/cookbook/merge_objects.md diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs index a25346b1..a9bff707 100644 --- a/apps/website/docs/.vitepress/config.mjs +++ b/apps/website/docs/.vitepress/config.mjs @@ -128,6 +128,10 @@ export default defineConfig({ text: 'Custom Matchers', link: '/contracts/cookbook/custom_matchers', }, + { + text: 'Merge Objects', + link: '/contracts/cookbook/merge_objects', + }, ], }, { text: 'APIs', link: '/contracts/api' }, diff --git a/apps/website/docs/contracts/cookbook/merge_objects.md b/apps/website/docs/contracts/cookbook/merge_objects.md new file mode 100644 index 00000000..d5d9cd5b --- /dev/null +++ b/apps/website/docs/contracts/cookbook/merge_objects.md @@ -0,0 +1,24 @@ +# Merge Objects + +Merge two [_Contracts_](/primitives/contract) representing objects into a single [_Contract_](/primitives/contract) representing an object with fields from both input objects is a common operation in many applications. + +With `@withease/contracts` in can be done with simple `and` call: + +```ts +import { num, str, obj, and, type UnContract } from '@withease/contracts'; + +const Price = obj({ + currency: str, + value: num, +}); + +const PriceWithDiscount = and( + Price, + obj({ + discount: num, + }) +); + +type TPriceWithDiscount = UnContract; +// 👆 { currency: string, value: number, discount: number } +``` diff --git a/packages/contracts/src/contract.test-d.ts b/packages/contracts/src/contract.test-d.ts index b7a03cc4..46c06d83 100644 --- a/packages/contracts/src/contract.test-d.ts +++ b/packages/contracts/src/contract.test-d.ts @@ -1,6 +1,6 @@ import { describe, test, expectTypeOf } from 'vitest'; -import { and, Contract, num } from './index'; +import { and, Contract, num, obj, str } from './index'; describe('and', () => { test('inline contract', () => { @@ -11,4 +11,12 @@ describe('and', () => { expectTypeOf(contract).toEqualTypeOf>(); }); + + test('as extends', () => { + const contract = and(obj({ name: str }), obj({ age: num })); + + expectTypeOf(contract).toEqualTypeOf< + Contract + >(); + }); }); diff --git a/packages/contracts/src/contracts.test.ts b/packages/contracts/src/contracts.test.ts index 00f3f70b..63e5bde4 100644 --- a/packages/contracts/src/contracts.test.ts +++ b/packages/contracts/src/contracts.test.ts @@ -547,3 +547,27 @@ describe('tuple', () => { `); }); }); + +describe('special cases', () => { + it('or with two big objects', () => { + const cntrct = or(obj({ name: str }), obj({ age: num })); + + expect(cntrct.getErrorMessages({ lol: 'kek' })).toMatchInlineSnapshot(` + [ + "name: expected string, got undefined", + "age: expected number, got undefined", + ] + `); + }); + + it('and as extends', () => { + const contract = and(obj({ name: str }), obj({ age: num })); + + expect(contract.isData({ name: 'a', age: 1 })).toBeTruthy(); + + expect(contract.isData({ name: 'a' })).toBeFalsy(); + expect(contract.isData({ age: 1 })).toBeFalsy(); + expect(contract.isData({ name: 'a', age: 'ERROR' })).toBeFalsy(); + expect(contract.isData({ name: 18888, age: 1 })).toBeFalsy(); + }); +}); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index d141c26f..e4ec1495 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -108,9 +108,42 @@ export function or>>( }; } +type Merge = { + [K in keyof T | keyof U]: K extends keyof U + ? U[K] + : K extends keyof T + ? T[K] + : never; +}; + +/** + * Function that merges two _Contracts_ of objects into one. + * + * @overload "and(objectA, objectB)" + * + * @example + * + * const User = obj({ + * name: str, + * }); + * + * const Admin = and(User, obj({ + * permitted: bool, + * })); + */ +export function and< + T extends Record, + K extends Record +>( + first: Contract, + second: Contract +): Contract>; + /** * Function that creates a _Contract_ that checks if a value is conform to all of the given _Contracts_. * + * @overload "and(first, ...rest)" + * * @example * function age(min, max): Contract { * return { @@ -125,6 +158,11 @@ export function or>>( * age: and(num, age(18, 100)), * }); */ +export function and( + first: Contract, + ...rest: Array> +): Contract; + export function and( first: Contract, ...rest: Array>